feat(secrets-ui): Improve SecretDashboard table to support multiple changes on a single commit

This commit is contained in:
Carlos Monastyrski
2025-07-10 11:17:45 -03:00
parent 78da7ec343
commit 246fe81134
24 changed files with 3177 additions and 499 deletions
@@ -1,4 +1,6 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError, subject } from "@casl/ability";
import { Knex } from "knex";
import path from "path";
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
@@ -12,14 +14,21 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import {
ChangeType,
CommitType,
TCreateCommitChangeDTO,
TFolderCommitServiceFactory
} from "../folder-commit/folder-commit-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal";
import {
TCreateFolderDTO,
TCreateManyFoldersDTO,
TDeleteFolderDTO,
TDeleteManyFoldersDTO,
TGetFolderByIdDTO,
TGetFolderDTO,
TGetFoldersDeepByEnvsDTO,
@@ -236,19 +245,26 @@ export const secretFolderServiceFactory = ({
actor,
actorId,
projectSlug,
projectId: providedProjectId,
actorAuthMethod,
actorOrgId,
folders
}: TUpdateManyFoldersDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) {
throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
folders,
tx: providedTx,
commitChanges
}: TUpdateManyFoldersDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[]; projectId?: string }) => {
let projectId = providedProjectId;
if (!projectId) {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) {
throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
}
projectId = project.id;
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
projectId,
actorAuthMethod,
actorOrgId
});
@@ -260,12 +276,12 @@ export const secretFolderServiceFactory = ({
);
});
const result = await folderDAL.transaction(async (tx) =>
Promise.all(
const executeBulkUpdate = async (tx: Knex) => {
return Promise.all(
folders.map(async (newFolder) => {
const { environment, path: secretPath, id, name, description } = newFolder;
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
const parentFolder = await folderDAL.findBySecretPath(projectId as string, environment, secretPath, tx);
if (!parentFolder) {
throw new NotFoundError({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`,
@@ -273,10 +289,10 @@ export const secretFolderServiceFactory = ({
});
}
const env = await projectEnvDAL.findOne({ projectId: project.id, slug: environment });
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) {
throw new NotFoundError({
message: `Environment with slug '${environment}' in project with ID '${project.id}' not found`,
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`,
name: "UpdateManyFolders"
});
}
@@ -323,26 +339,34 @@ export const secretFolderServiceFactory = ({
},
tx
);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
if (commitChanges) {
commitChanges.push({
type: CommitType.ADD,
isUpdate: true,
folderVersionId: folderVersion.id
});
} else {
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder updated",
folderId: parentFolder.id,
changes: [
{
type: CommitType.ADD,
isUpdate: true,
folderVersionId: folderVersion.id
}
]
},
message: "Folder updated",
folderId: parentFolder.id,
changes: [
{
type: CommitType.ADD,
isUpdate: true,
folderVersionId: folderVersion.id
}
]
},
tx
);
tx
);
}
if (!doc) {
throw new NotFoundError({
message: `Failed to update folder with id '${id}', not found`,
@@ -352,13 +376,16 @@ export const secretFolderServiceFactory = ({
return { oldFolder: folder, newFolder: doc };
})
)
);
);
};
// Execute with provided transaction or create new one
const result = providedTx ? await executeBulkUpdate(providedTx) : await folderDAL.transaction(executeBulkUpdate);
await Promise.all(result.map(async (res) => snapshotService.performSnapshot(res.newFolder.parentId as string)));
return {
projectId: project.id,
projectId,
newFolders: result.map((res) => res.newFolder),
oldFolders: result.map((res) => res.oldFolder)
};
@@ -974,6 +1001,361 @@ export const secretFolderServiceFactory = ({
}));
};
const createManyFolders = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
folders,
tx: providedTx,
commitChanges
}: TCreateManyFoldersDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[] }) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
});
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
});
const foldersByEnv = folders.reduce(
(acc, folder) => {
if (!acc[folder.environment]) {
acc[folder.environment] = [];
}
acc[folder.environment].push(folder);
return acc;
},
{} as Record<string, typeof folders>
);
const executeBulkCreate = async (tx: Knex) => {
const createdFolders = [];
for (const [environment, envFolders] of Object.entries(foldersByEnv)) {
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) {
throw new NotFoundError({
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`
});
}
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateFolder(env.id, env.projectId)]);
for (const folderSpec of envFolders) {
const { name, path: secretPath, description } = folderSpec;
const pathWithFolder = path.join(secretPath, name);
const parentFolder = await folderDAL.findClosestFolder(projectId, environment, pathWithFolder, tx);
if (!parentFolder) {
throw new NotFoundError({
message: `Parent folder for path '${pathWithFolder}' not found`
});
}
// Check if the exact folder already exists
const existingFolder = await folderDAL.findOne(
{
envId: env.id,
parentId: parentFolder.id,
name,
isReserved: false
},
tx
);
if (existingFolder) {
createdFolders.push(existingFolder);
// eslint-disable-next-line no-continue
continue;
}
// Handle exact folder case
if (parentFolder.path === pathWithFolder) {
createdFolders.push(parentFolder);
// eslint-disable-next-line no-continue
continue;
}
let currentParentId = parentFolder.id;
// Build the full path we need by processing each segment
if (parentFolder.path !== secretPath) {
const missingSegments = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
const newFolders: TSecretFoldersInsert[] = [];
for (const segment of missingSegments) {
const existingSegment = await folderDAL.findOne(
{
name: segment,
parentId: currentParentId,
envId: env.id,
isReserved: false
},
tx
);
if (existingSegment) {
currentParentId = existingSegment.id;
} else {
const newFolder = {
name: segment,
parentId: currentParentId,
id: uuidv4(),
envId: env.id,
version: 1
};
currentParentId = newFolder.id;
newFolders.push(newFolder);
}
}
if (newFolders.length) {
const docs = await folderDAL.insertMany(newFolders, tx);
const folderVersions = await folderVersionDAL.insertMany(
docs.map((doc) => ({
name: doc.name,
envId: doc.envId,
version: doc.version,
folderId: doc.id,
description: doc.description
})),
tx
);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folders created (batch)",
folderId: currentParentId,
changes: folderVersions.map((fv) => ({
type: CommitType.ADD,
folderVersionId: fv.id
}))
},
tx
);
}
}
// Create the target folder
const doc = await folderDAL.create(
{ name, envId: env.id, version: 1, parentId: currentParentId, description },
tx
);
const folderVersion = await folderVersionDAL.create(
{
name: doc.name,
envId: doc.envId,
version: doc.version,
folderId: doc.id,
description: doc.description
},
tx
);
if (commitChanges) {
commitChanges.push({
type: CommitType.ADD,
folderVersionId: folderVersion.id
});
} else {
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder created (batch)",
folderId: doc.id,
changes: [
{
type: CommitType.ADD,
folderVersionId: folderVersion.id
}
]
},
tx
);
}
createdFolders.push(doc);
}
}
return createdFolders;
};
const result = providedTx ? await executeBulkCreate(providedTx) : await folderDAL.transaction(executeBulkCreate);
const uniqueParentIds = [...new Set(result.map((folder) => folder.parentId).filter(Boolean))];
await Promise.all(uniqueParentIds.map((parentId) => snapshotService.performSnapshot(parentId as string)));
return {
folders: result,
count: result.length
};
};
const deleteManyFolders = async ({
projectId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
folders,
tx: providedTx,
commitChanges
}: TDeleteManyFoldersDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[] }) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
});
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
});
const foldersByEnv = folders.reduce(
(acc, folder) => {
if (!acc[folder.environment]) {
acc[folder.environment] = [];
}
acc[folder.environment].push(folder);
return acc;
},
{} as Record<string, typeof folders>
);
const executeBulkDelete = async (tx: Knex) => {
const deletedFolders = [];
for (const [environment, envFolders] of Object.entries(foldersByEnv)) {
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) {
throw new NotFoundError({
message: `Environment with slug '${environment}' not found`
});
}
for (const folderSpec of envFolders) {
const { path: secretPath, idOrName } = folderSpec;
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, tx);
if (!parentFolder) {
throw new NotFoundError({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
});
}
await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName });
let folderToDelete = await folderDAL
.findOne({
envId: env.id,
name: idOrName,
parentId: parentFolder.id,
isReserved: false
})
.catch(() => null);
if (!folderToDelete && uuidValidate(idOrName)) {
folderToDelete = await folderDAL
.findOne({
envId: env.id,
id: idOrName,
parentId: parentFolder.id,
isReserved: false
})
.catch(() => null);
}
if (!folderToDelete) {
throw new NotFoundError({
message: `Folder with ID/name '${idOrName}' not found`
});
}
const [doc] = await folderDAL.delete(
{
envId: env.id,
id: folderToDelete.id,
parentId: parentFolder.id,
isReserved: false
},
tx
);
const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id], tx);
if (commitChanges) {
commitChanges.push({
type: CommitType.DELETE,
folderVersionId: folderVersions[doc.id].id,
folderId: doc.id
});
} else {
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder deleted (batch)",
folderId: parentFolder.id,
changes: [
{
type: CommitType.DELETE,
folderVersionId: folderVersions[doc.id].id,
folderId: doc.id
}
]
},
tx
);
}
deletedFolders.push(doc);
}
}
return deletedFolders;
};
const result = providedTx ? await executeBulkDelete(providedTx) : await folderDAL.transaction(executeBulkDelete);
const uniqueParentIds = [...new Set(result.map((folder) => folder.parentId).filter(Boolean))];
await Promise.all(uniqueParentIds.map((parentId) => snapshotService.performSnapshot(parentId as string)));
return {
folders: result,
count: result.length
};
};
return {
createFolder,
updateFolder,
@@ -986,6 +1368,8 @@ export const secretFolderServiceFactory = ({
getFoldersDeepByEnvs,
getProjectEnvironmentsFolders,
getFolderVersionsByIds,
getFolderVersions
getFolderVersions,
createManyFolders,
deleteManyFolders
};
};
@@ -1,6 +1,8 @@
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export enum ReservedFolders {
SecretReplication = "__reserve_replication_"
}
@@ -62,3 +64,30 @@ export type TGetFoldersDeepByEnvsDTO = {
export type TFindFoldersDeepByParentIdsDTO = {
parentIds: string[];
};
export type TCreateManyFoldersDTO = {
projectId: string;
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId?: string;
folders: Array<{
name: string;
environment: string;
path: string;
description?: string;
}>;
};
export type TDeleteManyFoldersDTO = {
projectId: string;
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId?: string;
folders: Array<{
environment: string;
path: string;
idOrName: string;
}>;
};