mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 03:26:12 +00:00
feat: added batch update endpoint for folders
This commit is contained in:
@@ -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.
|
// TODO(daniel): Expose this route in api reference and write docs for it.
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
|
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-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";
|
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
|
||||||
|
|
||||||
type TSecretFolderServiceFactoryDep = {
|
type TSecretFolderServiceFactoryDep = {
|
||||||
@@ -116,6 +122,93 @@ export const secretFolderServiceFactory = ({
|
|||||||
return folder;
|
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 ({
|
const updateFolder = async ({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
@@ -254,6 +347,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
createFolder,
|
createFolder,
|
||||||
updateFolder,
|
updateFolder,
|
||||||
|
updateManyFolders,
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
getFolders
|
getFolders
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ export type TUpdateFolderDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateManyFoldersDTO = {
|
||||||
|
folders: {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteFolderDTO = {
|
export type TDeleteFolderDTO = {
|
||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user