feat: added batch update endpoint for folders

This commit is contained in:
Sheen Capadngan
2024-05-10 13:57:00 +08:00
parent 522dd0836e
commit a6f750fafb
3 changed files with 169 additions and 1 deletions

View File

@@ -127,6 +127,71 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}
});
server.route({
url: "/batch",
method: "PATCH",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Update folders by batch",
security: [
{
bearerAuth: []
}
],
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
folders: z
.object({
id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path)
})
.array()
.min(1)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { newFolders, oldFolders } = await server.services.folder.updateManyFolders({
folders: req.body.folders,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId
});
await Promise.all(
req.body.folders.map(async (folder, ind) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.UPDATE_FOLDER,
metadata: {
environment: oldFolders[ind].envId,
folderId: oldFolders[ind].id,
folderPath: folder.path,
newFolderName: newFolders[ind].name,
oldFolderName: oldFolders[ind].name
}
}
});
})
);
return { folders: newFolders };
}
});
// TODO(daniel): Expose this route in api reference and write docs for it.
server.route({
method: "DELETE",

View File

@@ -10,7 +10,13 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal";
import { TCreateFolderDTO, TDeleteFolderDTO, TGetFolderDTO, TUpdateFolderDTO } from "./secret-folder-types";
import {
TCreateFolderDTO,
TDeleteFolderDTO,
TGetFolderDTO,
TUpdateFolderDTO,
TUpdateManyFoldersDTO
} from "./secret-folder-types";
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
type TSecretFolderServiceFactoryDep = {
@@ -116,6 +122,93 @@ export const secretFolderServiceFactory = ({
return folder;
};
const updateManyFolders = async ({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
folders
}: TUpdateManyFoldersDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
});
const result = await folderDAL.transaction(async (tx) =>
Promise.all(
folders.map(async (newFolder) => {
const { environment, path: secretPath, id, name } = newFolder;
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Update folder" });
const folder = await folderDAL
.findOne({ envId: env.id, id, parentId: parentFolder.id })
// now folder api accepts id based change
// this is for cli backward compatiability and when cli removes this, we will remove this logic
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
if (name !== folder.name) {
// ensure that new folder name is unique
const folderToCheck = await folderDAL.findOne({
name,
envId: env.id,
parentId: parentFolder.id
});
if (folderToCheck) {
throw new BadRequestError({
message: "Folder with specified name already exists",
name: "Batch update folder"
});
}
}
const [doc] = await folderDAL.update(
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
{ name },
tx
);
await folderVersionDAL.create(
{
name: doc.name,
envId: doc.envId,
version: doc.version,
folderId: doc.id
},
tx
);
if (!doc) {
throw new BadRequestError({ message: "Folder not found", name: "Batch update folder" });
}
return { oldFolder: folder, newFolder: doc };
})
)
);
return {
newFolders: result.map((res) => res.newFolder),
oldFolders: result.map((res) => res.oldFolder)
};
};
const updateFolder = async ({
projectId,
actor,
@@ -254,6 +347,7 @@ export const secretFolderServiceFactory = ({
return {
createFolder,
updateFolder,
updateManyFolders,
deleteFolder,
getFolders
};

View File

@@ -13,6 +13,15 @@ export type TUpdateFolderDTO = {
name: string;
} & TProjectPermission;
export type TUpdateManyFoldersDTO = {
folders: {
environment: string;
path: string;
id: string;
name: string;
}[];
} & TProjectPermission;
export type TDeleteFolderDTO = {
environment: string;
path: string;