chore(core): Expose owner information for environment changes (#16466)

Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
This commit is contained in:
Andreas Fitzek
2025-06-20 11:33:17 +02:00
committed by GitHub
parent 37a4c4b73e
commit 57911225e7
12 changed files with 223 additions and 72 deletions

View File

@@ -19,6 +19,12 @@ export const SOURCE_CONTROL_FILE_STATUS = FileStatusSchema.Values;
const FileLocationSchema = z.enum(['local', 'remote']); const FileLocationSchema = z.enum(['local', 'remote']);
export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values; export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values;
const ResourceOwnerSchema = z.object({
type: z.enum(['personal', 'team']),
projectId: z.string(),
projectName: z.string(),
});
export const SourceControlledFileSchema = z.object({ export const SourceControlledFileSchema = z.object({
file: z.string(), file: z.string(),
id: z.string(), id: z.string(),
@@ -29,6 +35,7 @@ export const SourceControlledFileSchema = z.object({
conflict: z.boolean(), conflict: z.boolean(),
updatedAt: z.string(), updatedAt: z.string(),
pushed: z.boolean().optional(), pushed: z.boolean().optional(),
owner: ResourceOwnerSchema.optional(), // Resource owner can be a personal email or team information
}); });
export type SourceControlledFile = z.infer<typeof SourceControlledFileSchema>; export type SourceControlledFile = z.infer<typeof SourceControlledFileSchema>;

View File

@@ -66,9 +66,14 @@ describe('SourceControlImportService', () => {
id: 'workflow1', id: 'workflow1',
versionId: 'v1', versionId: 'v1',
name: 'Test Workflow', name: 'Test Workflow',
owner: {
type: 'personal',
personalEmail: 'email@email.com',
},
}; };
fsReadFile.mockResolvedValue(JSON.stringify(mockWorkflowData)); fsReadFile.mockResolvedValue(JSON.stringify(mockWorkflowData));
sourceControlScopedService.getAdminProjectsFromContext.mockResolvedValueOnce([]);
const result = await service.getRemoteVersionIdsFromFiles(globalAdminContext); const result = await service.getRemoteVersionIdsFromFiles(globalAdminContext);
expect(fsReadFile).toHaveBeenCalledWith(mockWorkflowFile, { encoding: 'utf8' }); expect(fsReadFile).toHaveBeenCalledWith(mockWorkflowFile, { encoding: 'utf8' });

View File

@@ -14,7 +14,7 @@ import { SourceControlService } from '@/environments.ee/source-control/source-co
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import type { SourceControlImportService } from '../source-control-import.service.ee'; import type { SourceControlImportService } from '../source-control-import.service.ee';
import type { ExportableCredential } from '../types/exportable-credential'; import type { StatusExportableCredential } from '../types/exportable-credential';
import type { SourceControlWorkflowVersionId } from '../types/source-control-workflow-version-id'; import type { SourceControlWorkflowVersionId } from '../types/source-control-workflow-version-id';
describe('SourceControlService', () => { describe('SourceControlService', () => {
@@ -157,7 +157,7 @@ describe('SourceControlService', () => {
// Pushing this is conflict free. // Pushing this is conflict free.
sourceControlImportService.getRemoteCredentialsFromFiles.mockResolvedValue([]); sourceControlImportService.getRemoteCredentialsFromFiles.mockResolvedValue([]);
sourceControlImportService.getLocalCredentialsFromDb.mockResolvedValue([ sourceControlImportService.getLocalCredentialsFromDb.mockResolvedValue([
mock<ExportableCredential & { filename: string }>(), mock<StatusExportableCredential>(),
]); ]);
// Define a variable that does only exist locally. // Define a variable that does only exist locally.

View File

@@ -40,7 +40,7 @@ import { SourceControlScopedService } from './source-control-scoped.service';
import type { ExportResult } from './types/export-result'; import type { ExportResult } from './types/export-result';
import type { ExportableCredential } from './types/exportable-credential'; import type { ExportableCredential } from './types/exportable-credential';
import type { ExportableWorkflow } from './types/exportable-workflow'; import type { ExportableWorkflow } from './types/exportable-workflow';
import type { ResourceOwner } from './types/resource-owner'; import type { RemoteResourceOwner } from './types/resource-owner';
import type { SourceControlContext } from './types/source-control-context'; import type { SourceControlContext } from './types/source-control-context';
import { VariablesService } from '../variables/variables.service.ee'; import { VariablesService } from '../variables/variables.service.ee';
@@ -99,7 +99,7 @@ export class SourceControlExportService {
private async writeExportableWorkflowsToExportFolder( private async writeExportableWorkflowsToExportFolder(
workflowsToBeExported: IWorkflowDb[], workflowsToBeExported: IWorkflowDb[],
owners: Record<string, ResourceOwner>, owners: Record<string, RemoteResourceOwner>,
) { ) {
await Promise.all( await Promise.all(
workflowsToBeExported.map(async (e) => { workflowsToBeExported.map(async (e) => {
@@ -133,7 +133,7 @@ export class SourceControlExportService {
}); });
// determine owner of each workflow to be exported // determine owner of each workflow to be exported
const owners: Record<string, ResourceOwner> = {}; const owners: Record<string, RemoteResourceOwner> = {};
sharedWorkflows.forEach((sharedWorkflow) => { sharedWorkflows.forEach((sharedWorkflow) => {
const project = sharedWorkflow.project; const project = sharedWorkflow.project;
@@ -154,6 +154,8 @@ export class SourceControlExportService {
} }
owners[sharedWorkflow.workflowId] = { owners[sharedWorkflow.workflowId] = {
type: 'personal', type: 'personal',
projectId: project.id,
projectName: project.name,
personalEmail: ownerRelation.user.email, personalEmail: ownerRelation.user.email,
}; };
} else if (project.type === 'team') { } else if (project.type === 'team') {
@@ -256,12 +258,11 @@ export class SourceControlExportService {
// keep all folders that are not accessible by the current user // keep all folders that are not accessible by the current user
// if allowedProjects is undefined, all folders are accessible by the current user // if allowedProjects is undefined, all folders are accessible by the current user
const foldersToKeepUnchanged = const foldersToKeepUnchanged = context.hasAccessToAllProjects()
allowedProjects === undefined ? existingFolders.folders
? [] : existingFolders.folders.filter((folder) => {
: existingFolders.folders.filter((folder) => { return !allowedProjects.some((project) => project.id === folder.homeProjectId);
return !allowedProjects.some((project) => project.id === folder.homeProjectId); });
});
const newFolders = foldersToKeepUnchanged.concat( const newFolders = foldersToKeepUnchanged.concat(
...folders.map((f) => ({ ...folders.map((f) => ({
@@ -402,7 +403,7 @@ export class SourceControlExportService {
const { name, type, data, id } = sharing.credentials; const { name, type, data, id } = sharing.credentials;
const credentials = new Credentials({ id, name }, type, data); const credentials = new Credentials({ id, name }, type, data);
let owner: ResourceOwner | null = null; let owner: RemoteResourceOwner | null = null;
if (sharing.project.type === 'personal') { if (sharing.project.type === 'personal') {
const ownerRelation = sharing.project.projectRelations.find( const ownerRelation = sharing.project.projectRelations.find(
(pr) => pr.role === 'project:personalOwner', (pr) => pr.role === 'project:personalOwner',
@@ -410,6 +411,8 @@ export class SourceControlExportService {
if (ownerRelation) { if (ownerRelation) {
owner = { owner = {
type: 'personal', type: 'personal',
projectId: sharing.project.id,
projectName: sharing.project.name,
personalEmail: ownerRelation.user.email, personalEmail: ownerRelation.user.email,
}; };
} }

View File

@@ -41,14 +41,67 @@ import {
} from './constants'; } from './constants';
import { getCredentialExportPath, getWorkflowExportPath } from './source-control-helper.ee'; import { getCredentialExportPath, getWorkflowExportPath } from './source-control-helper.ee';
import { SourceControlScopedService } from './source-control-scoped.service'; import { SourceControlScopedService } from './source-control-scoped.service';
import type { ExportableCredential } from './types/exportable-credential'; import type {
ExportableCredential,
StatusExportableCredential,
} from './types/exportable-credential';
import type { ExportableFolder } from './types/exportable-folders'; import type { ExportableFolder } from './types/exportable-folders';
import type { ExportableTags } from './types/exportable-tags'; import type { ExportableTags } from './types/exportable-tags';
import type { ResourceOwner } from './types/resource-owner'; import type { StatusResourceOwner, RemoteResourceOwner } from './types/resource-owner';
import type { SourceControlContext } from './types/source-control-context'; import type { SourceControlContext } from './types/source-control-context';
import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id'; import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id';
import { VariablesService } from '../variables/variables.service.ee'; import { VariablesService } from '../variables/variables.service.ee';
const findOwnerProject = (
owner: RemoteResourceOwner,
accessibleProjects: Project[],
): Project | undefined => {
if (typeof owner === 'string') {
return accessibleProjects.find((project) =>
project.projectRelations.some(
(r) => r.role === 'project:personalOwner' && r.user.email === owner,
),
);
}
if (owner.type === 'personal') {
return accessibleProjects.find(
(project) =>
project.type === 'personal' &&
project.projectRelations.some(
(r) => r.role === 'project:personalOwner' && r.user.email === owner.personalEmail,
),
);
}
return accessibleProjects.find(
(project) => project.type === 'team' && project.id === owner.teamId,
);
};
const getOwnerFromProject = (remoteOwnerProject: Project): StatusResourceOwner | undefined => {
let owner: StatusResourceOwner | undefined = undefined;
if (remoteOwnerProject?.type === 'personal') {
const personalEmail = remoteOwnerProject.projectRelations?.find(
(r) => r.role === 'project:personalOwner',
)?.user?.email;
if (personalEmail) {
owner = {
type: 'personal',
projectId: remoteOwnerProject.id,
projectName: remoteOwnerProject.name,
};
}
} else if (remoteOwnerProject?.type === 'team') {
owner = {
type: 'team',
projectId: remoteOwnerProject.id,
projectName: remoteOwnerProject.name,
};
}
return owner;
};
@Service() @Service()
export class SourceControlImportService { export class SourceControlImportService {
private gitFolder: string; private gitFolder: string;
@@ -109,26 +162,21 @@ export class SourceControlImportService {
if (!remote?.id) { if (!remote?.id) {
return false; return false;
} }
if (Array.isArray(accessibleProjects)) { return (
const owner = remote.owner; context.hasAccessToAllProjects() || findOwnerProject(remote.owner, accessibleProjects)
// The workflow `remote` belongs not to a project, that the context has access to );
return (
typeof owner === 'object' &&
owner?.type === 'team' &&
accessibleProjects.some((project) => project.id === owner.teamId)
);
}
return true;
}) })
.map((remote) => { .map((remote) => {
const project = findOwnerProject(remote.owner, accessibleProjects);
return { return {
id: remote.id, id: remote.id,
versionId: remote.versionId, versionId: remote.versionId ?? '',
name: remote.name, name: remote.name,
parentFolderId: remote.parentFolderId, parentFolderId: remote.parentFolderId,
remoteId: remote.id, remoteId: remote.id,
filename: getWorkflowExportPath(remote.id, this.workflowExportFolder), filename: getWorkflowExportPath(remote.id, this.workflowExportFolder),
} as SourceControlWorkflowVersionId; owner: project ? getOwnerFromProject(project) : undefined,
};
}); });
return remoteWorkflowFilesParsed; return remoteWorkflowFilesParsed;
@@ -178,6 +226,13 @@ export class SourceControlImportService {
const localWorkflows = await this.workflowRepository.find({ const localWorkflows = await this.workflowRepository.find({
relations: { relations: {
parentFolder: true, parentFolder: true,
shared: {
project: {
projectRelations: {
user: true,
},
},
},
}, },
select: { select: {
id: true, id: true,
@@ -187,9 +242,24 @@ export class SourceControlImportService {
parentFolder: { parentFolder: {
id: true, id: true,
}, },
shared: {
project: {
id: true,
name: true,
type: true,
projectRelations: {
role: true,
user: {
email: true,
},
},
},
role: true,
},
}, },
where: this.sourceControlScopedService.getWorkflowsInAdminProjectsFromContextFilter(context), where: this.sourceControlScopedService.getWorkflowsInAdminProjectsFromContextFilter(context),
}); });
return localWorkflows.map((local) => { return localWorkflows.map((local) => {
let updatedAt: Date; let updatedAt: Date;
if (local.updatedAt instanceof Date) { if (local.updatedAt instanceof Date) {
@@ -203,6 +273,8 @@ export class SourceControlImportService {
}); });
updatedAt = isNaN(Date.parse(local.updatedAt)) ? new Date() : new Date(local.updatedAt); updatedAt = isNaN(Date.parse(local.updatedAt)) ? new Date() : new Date(local.updatedAt);
} }
const remoteOwnerProject = local.shared?.find((s) => s.role === 'workflow:owner')?.project;
return { return {
id: local.id, id: local.id,
versionId: local.versionId, versionId: local.versionId,
@@ -211,13 +283,14 @@ export class SourceControlImportService {
parentFolderId: local.parentFolder?.id ?? null, parentFolderId: local.parentFolder?.id ?? null,
filename: getWorkflowExportPath(local.id, this.workflowExportFolder), filename: getWorkflowExportPath(local.id, this.workflowExportFolder),
updatedAt: updatedAt.toISOString(), updatedAt: updatedAt.toISOString(),
owner: remoteOwnerProject ? getOwnerFromProject(remoteOwnerProject) : undefined,
}; };
}) as SourceControlWorkflowVersionId[]; });
} }
async getRemoteCredentialsFromFiles( async getRemoteCredentialsFromFiles(
context: SourceControlContext, context: SourceControlContext,
): Promise<Array<ExportableCredential & { filename: string }>> { ): Promise<StatusExportableCredential[]> {
const remoteCredentialFiles = await glob('*.json', { const remoteCredentialFiles = await glob('*.json', {
cwd: this.credentialExportFolder, cwd: this.credentialExportFolder,
absolute: true, absolute: true,
@@ -241,43 +314,79 @@ export class SourceControlImportService {
if (!remote?.id) { if (!remote?.id) {
return false; return false;
} }
if (Array.isArray(accessibleProjects)) { const owner = remote.ownedBy;
const owner = remote.ownedBy; // The credential `remote` belongs not to a project, that the context has access to
// The credential `remote` belongs not to a project, that the context has access to return (
return ( !owner || context.hasAccessToAllProjects() || findOwnerProject(owner, accessibleProjects)
typeof owner === 'object' && );
owner?.type === 'team' &&
accessibleProjects.some((project) => project.id === owner.teamId)
);
}
return true;
}) })
.map((remote) => { .map((remote) => {
const project = remote.ownedBy
? findOwnerProject(remote.ownedBy, accessibleProjects)
: null;
return { return {
...remote, ...remote,
ownedBy: project
? {
type: project.type,
projectId: project.id,
projectName: project.name,
}
: undefined,
filename: getCredentialExportPath(remote.id, this.credentialExportFolder), filename: getCredentialExportPath(remote.id, this.credentialExportFolder),
}; };
}); });
return remoteCredentialFilesParsed.filter((e) => e !== undefined) as Array< return remoteCredentialFilesParsed.filter(
ExportableCredential & { filename: string } (e) => e !== undefined,
>; ) as StatusExportableCredential[];
} }
async getLocalCredentialsFromDb( async getLocalCredentialsFromDb(
context: SourceControlContext, context: SourceControlContext,
): Promise<Array<ExportableCredential & { filename: string }>> { ): Promise<StatusExportableCredential[]> {
const localCredentials = await this.credentialsRepository.find({ const localCredentials = await this.credentialsRepository.find({
select: ['id', 'name', 'type'], relations: {
shared: {
project: {
projectRelations: {
user: true,
},
},
},
},
select: {
id: true,
name: true,
type: true,
shared: {
project: {
id: true,
name: true,
type: true,
projectRelations: {
role: true,
user: {
email: true,
},
},
},
role: true,
},
},
where: where:
this.sourceControlScopedService.getCredentialsInAdminProjectsFromContextFilter(context), this.sourceControlScopedService.getCredentialsInAdminProjectsFromContextFilter(context),
}); });
return localCredentials.map((local) => ({ return localCredentials.map((local) => {
id: local.id, const remoteOwnerProject = local.shared?.find((s) => s.role === 'credential:owner')?.project;
name: local.name, return {
type: local.type, id: local.id,
filename: getCredentialExportPath(local.id, this.credentialExportFolder), name: local.name,
})) as Array<ExportableCredential & { filename: string }>; type: local.type,
filename: getCredentialExportPath(local.id, this.credentialExportFolder),
ownedBy: remoteOwnerProject ? getOwnerFromProject(remoteOwnerProject) : undefined,
};
}) as StatusExportableCredential[];
} }
async getRemoteVariablesFromFile(): Promise<Variables[]> { async getRemoteVariablesFromFile(): Promise<Variables[]> {
@@ -316,11 +425,11 @@ export class SourceControlImportService {
const accessibleProjects = const accessibleProjects =
await this.sourceControlScopedService.getAdminProjectsFromContext(context); await this.sourceControlScopedService.getAdminProjectsFromContext(context);
if (Array.isArray(accessibleProjects)) { mappedFolders.folders = mappedFolders.folders.filter(
mappedFolders.folders = mappedFolders.folders.filter((folder) => (folder) =>
context.hasAccessToAllProjects() ||
accessibleProjects.some((project) => project.id === folder.homeProjectId), accessibleProjects.some((project) => project.id === folder.homeProjectId),
); );
}
return mappedFolders; return mappedFolders;
} }
@@ -796,7 +905,7 @@ export class SourceControlImportService {
} }
} }
private async findOrCreateOwnerProject(owner: ResourceOwner): Promise<Project | null> { private async findOrCreateOwnerProject(owner: RemoteResourceOwner): Promise<Project | null> {
if (typeof owner === 'string' || owner.type === 'personal') { if (typeof owner === 'string' || owner.type === 'personal') {
const email = typeof owner === 'string' ? owner : owner.personalEmail; const email = typeof owner === 'string' ? owner : owner.personalEmail;
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
@@ -834,7 +943,7 @@ export class SourceControlImportService {
assertNever(owner); assertNever(owner);
const errorOwner = owner as ResourceOwner; const errorOwner = owner as RemoteResourceOwner;
throw new UnexpectedError( throw new UnexpectedError(
`Unknown resource owner type "${ `Unknown resource owner type "${
typeof errorOwner !== 'string' ? errorOwner.type : 'UNKNOWN' typeof errorOwner !== 'string' ? errorOwner.type : 'UNKNOWN'

View File

@@ -37,19 +37,28 @@ export class SourceControlScopedService {
} }
} }
async getAdminProjectsFromContext(context: SourceControlContext): Promise<Project[] | undefined> { async getAdminProjectsFromContext(context: SourceControlContext): Promise<Project[]> {
if (context.hasAccessToAllProjects()) { if (context.hasAccessToAllProjects()) {
// In case the user is a global admin or owner, we don't need a filter // In case the user is a global admin or owner, we don't need a filter
return; return await this.projectRepository.find({
relations: {
projectRelations: {
user: true,
},
},
});
} }
return await this.projectRepository.find({ return await this.projectRepository.find({
relations: { relations: {
projectRelations: true, projectRelations: {
user: true,
},
}, },
select: { select: {
id: true, id: true,
name: true, name: true,
type: true,
}, },
where: this.getAdminProjectsByContextFilter(context), where: this.getAdminProjectsByContextFilter(context),
}); });

View File

@@ -42,7 +42,7 @@ import {
} from './source-control-helper.ee'; } from './source-control-helper.ee';
import { SourceControlImportService } from './source-control-import.service.ee'; import { SourceControlImportService } from './source-control-import.service.ee';
import { SourceControlPreferencesService } from './source-control-preferences.service.ee'; import { SourceControlPreferencesService } from './source-control-preferences.service.ee';
import type { ExportableCredential } from './types/exportable-credential'; import type { StatusExportableCredential } from './types/exportable-credential';
import type { ExportableFolder } from './types/exportable-folders'; import type { ExportableFolder } from './types/exportable-folders';
import type { ImportResult } from './types/import-result'; import type { ImportResult } from './types/import-result';
import { SourceControlContext } from './types/source-control-context'; import { SourceControlContext } from './types/source-control-context';
@@ -663,6 +663,7 @@ export class SourceControlService {
conflict: false, conflict: false,
file: item.filename, file: item.filename,
updatedAt: item.updatedAt ?? new Date().toISOString(), updatedAt: item.updatedAt ?? new Date().toISOString(),
owner: item.owner,
}); });
}); });
@@ -676,6 +677,7 @@ export class SourceControlService {
conflict: options.direction === 'push' ? false : true, conflict: options.direction === 'push' ? false : true,
file: item.filename, file: item.filename,
updatedAt: item.updatedAt ?? new Date().toISOString(), updatedAt: item.updatedAt ?? new Date().toISOString(),
owner: item.owner,
}); });
}); });
@@ -689,6 +691,7 @@ export class SourceControlService {
conflict: true, conflict: true,
file: item.filename, file: item.filename,
updatedAt: item.updatedAt ?? new Date().toISOString(), updatedAt: item.updatedAt ?? new Date().toISOString(),
owner: item.owner,
}); });
}); });
@@ -719,11 +722,7 @@ export class SourceControlService {
); );
// only compares the name, since that is the only change synced for credentials // only compares the name, since that is the only change synced for credentials
const credModifiedInEither: Array< const credModifiedInEither: StatusExportableCredential[] = [];
ExportableCredential & {
filename: string;
}
> = [];
credLocalIds.forEach((local) => { credLocalIds.forEach((local) => {
const mismatchingCreds = credRemoteIds.find((remote) => { const mismatchingCreds = credRemoteIds.find((remote) => {
return remote.id === local.id && (remote.name !== local.name || remote.type !== local.type); return remote.id === local.id && (remote.name !== local.name || remote.type !== local.type);
@@ -746,6 +745,7 @@ export class SourceControlService {
conflict: false, conflict: false,
file: item.filename, file: item.filename,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
owner: item.ownedBy,
}); });
}); });
@@ -759,6 +759,7 @@ export class SourceControlService {
conflict: options.direction === 'push' ? false : true, conflict: options.direction === 'push' ? false : true,
file: item.filename, file: item.filename,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
owner: item.ownedBy,
}); });
}); });
@@ -772,6 +773,7 @@ export class SourceControlService {
conflict: true, conflict: true,
file: item.filename, file: item.filename,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
owner: item.ownedBy,
}); });
}); });
return { return {

View File

@@ -1,6 +1,6 @@
import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import type { ResourceOwner } from './resource-owner'; import type { RemoteResourceOwner, StatusResourceOwner } from './resource-owner';
export interface ExportableCredential { export interface ExportableCredential {
id: string; id: string;
@@ -12,5 +12,10 @@ export interface ExportableCredential {
* Email of the user who owns this credential at the source instance. * Email of the user who owns this credential at the source instance.
* Ownership is mirrored at target instance if user is also present there. * Ownership is mirrored at target instance if user is also present there.
*/ */
ownedBy: ResourceOwner | null; ownedBy: RemoteResourceOwner | null;
} }
export type StatusExportableCredential = ExportableCredential & {
filename: string;
ownedBy?: StatusResourceOwner;
};

View File

@@ -1,6 +1,6 @@
import type { INode, IConnections, IWorkflowSettings } from 'n8n-workflow'; import type { INode, IConnections, IWorkflowSettings } from 'n8n-workflow';
import type { ResourceOwner } from './resource-owner'; import type { RemoteResourceOwner } from './resource-owner';
export interface ExportableWorkflow { export interface ExportableWorkflow {
id: string; id: string;
@@ -10,7 +10,7 @@ export interface ExportableWorkflow {
settings?: IWorkflowSettings; settings?: IWorkflowSettings;
triggerCount: number; triggerCount: number;
versionId?: string; versionId?: string;
owner: ResourceOwner; owner: RemoteResourceOwner;
parentFolderId: string | null; parentFolderId: string | null;
isArchived: boolean; isArchived: boolean;
} }

View File

@@ -1,7 +1,9 @@
export type ResourceOwner = export type RemoteResourceOwner =
| string | string
| { | {
type: 'personal'; type: 'personal';
projectId?: string; // Optional for retrocompatibility
projectName?: string; // Optional for retrocompatibility
personalEmail: string; personalEmail: string;
} }
| { | {
@@ -9,3 +11,9 @@ export type ResourceOwner =
teamId: string; teamId: string;
teamName: string; teamName: string;
}; };
export type StatusResourceOwner = {
type: 'personal' | 'team';
projectId: string;
projectName: string;
};

View File

@@ -1,3 +1,5 @@
import type { StatusResourceOwner } from './resource-owner';
export interface SourceControlWorkflowVersionId { export interface SourceControlWorkflowVersionId {
id: string; id: string;
versionId: string; versionId: string;
@@ -7,4 +9,5 @@ export interface SourceControlWorkflowVersionId {
remoteId?: string; remoteId?: string;
parentFolderId: string | null; parentFolderId: string | null;
updatedAt?: string; updatedAt?: string;
owner?: StatusResourceOwner;
} }

View File

@@ -30,7 +30,7 @@ import { SourceControlService } from '@/environments.ee/source-control/source-co
import type { ExportableCredential } from '@/environments.ee/source-control/types/exportable-credential'; import type { ExportableCredential } from '@/environments.ee/source-control/types/exportable-credential';
import type { ExportableFolder } from '@/environments.ee/source-control/types/exportable-folders'; import type { ExportableFolder } from '@/environments.ee/source-control/types/exportable-folders';
import type { ExportableWorkflow } from '@/environments.ee/source-control/types/exportable-workflow'; import type { ExportableWorkflow } from '@/environments.ee/source-control/types/exportable-workflow';
import type { ResourceOwner } from '@/environments.ee/source-control/types/resource-owner'; import type { RemoteResourceOwner } from '@/environments.ee/source-control/types/resource-owner';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
@@ -68,7 +68,7 @@ function toExportableCredential(
cred: CredentialsEntity, cred: CredentialsEntity,
owner: Project | User, owner: Project | User,
): ExportableCredential { ): ExportableCredential {
let resourceOwner: ResourceOwner; let resourceOwner: RemoteResourceOwner;
if (owner instanceof Project) { if (owner instanceof Project) {
resourceOwner = { resourceOwner = {
@@ -97,7 +97,7 @@ function toExportableWorkflow(
owner: Project | User, owner: Project | User,
versionId?: string, versionId?: string,
): ExportableWorkflow { ): ExportableWorkflow {
let resourceOwner: ResourceOwner; let resourceOwner: RemoteResourceOwner;
if (owner instanceof Project) { if (owner instanceof Project) {
resourceOwner = { resourceOwner = {