PIT: fix blocker for deep rollbacks

This commit is contained in:
carlosmonastyrski
2025-05-21 09:08:12 -03:00
parent 44aa743d56
commit 2493bbbc97
8 changed files with 336 additions and 216 deletions
+27 -27
View File
@@ -17,44 +17,44 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0, environmentsUsed: 0,
identityLimit: null, identityLimit: null,
identitiesUsed: 0, identitiesUsed: 0,
dynamicSecret: false, dynamicSecret: true,
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: true,
ipAllowlisting: false, ipAllowlisting: true,
rbac: false, rbac: true,
githubOrgSync: false, githubOrgSync: true,
customRateLimits: false, customRateLimits: true,
customAlerts: false, customAlerts: true,
secretAccessInsights: false, secretAccessInsights: true,
auditLogs: false, auditLogs: true,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
auditLogStreams: false, auditLogStreams: true,
auditLogStreamLimit: 3, auditLogStreamLimit: 3,
samlSSO: false, samlSSO: true,
hsm: false, hsm: true,
oidcSSO: false, oidcSSO: true,
scim: false, scim: true,
ldap: false, ldap: true,
groups: false, groups: true,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
secretApproval: false, secretApproval: true,
secretRotation: false, secretRotation: true,
caCrl: false, caCrl: true,
instanceUserManagement: false, instanceUserManagement: true,
externalKms: false, externalKms: true,
rateLimits: { rateLimits: {
readLimit: 60, readLimit: 60,
writeLimit: 200, writeLimit: 200,
secretsLimit: 40 secretsLimit: 40
}, },
pkiEst: false, pkiEst: true,
enforceMfa: false, enforceMfa: true,
projectTemplates: false, projectTemplates: true,
kmip: false, kmip: true,
gateway: false, gateway: true,
sshHostGroups: false sshHostGroups: true
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -108,8 +108,8 @@ export const folderCheckpointDALFactory = (db: TDbClient) => {
`${TableName.FolderCheckpoint}.folderCommitId`, `${TableName.FolderCheckpoint}.folderCommitId`,
`${TableName.FolderCommit}.id` `${TableName.FolderCommit}.id`
) )
.where(`${TableName.FolderCommit}.folderId`, commit.folderId) .where(`${TableName.FolderCommit}.folderId`, "=", commit.folderId)
.where(`${TableName.FolderCommit}.commitId`, "<=", commit.commitId.toString()) .where(`${TableName.FolderCommit}.commitId`, "<=", commit.commitId)
.select(selectAllTableCols(TableName.FolderCheckpoint)) .select(selectAllTableCols(TableName.FolderCheckpoint))
.select( .select(
db.ref("actorMetadata").withSchema(TableName.FolderCommit), db.ref("actorMetadata").withSchema(TableName.FolderCommit),
@@ -294,12 +294,11 @@ export const folderCommitDALFactory = (db: TDbClient) => {
folderId?: string; folderId?: string;
envId?: string; envId?: string;
startCommitId?: string; startCommitId?: string;
endCommitId: string; endCommitId?: string;
tx?: Knex; tx?: Knex;
}): Promise<TFolderCommits[]> => { }): Promise<TFolderCommits[]> => {
try { try {
const docs = await (tx || db.replicaNode())(TableName.FolderCommit) const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
.where("commitId", "<=", endCommitId)
.where((qb) => { .where((qb) => {
if (envId) { if (envId) {
void qb.where(`${TableName.FolderCommit}.envId`, "=", envId); void qb.where(`${TableName.FolderCommit}.envId`, "=", envId);
@@ -307,6 +306,9 @@ export const folderCommitDALFactory = (db: TDbClient) => {
if (startCommitId) { if (startCommitId) {
void qb.where("commitId", ">=", startCommitId); void qb.where("commitId", ">=", startCommitId);
} }
if (endCommitId) {
void qb.where("commitId", "<=", endCommitId);
}
}) })
.select(selectAllTableCols(TableName.FolderCommit)) .select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc"); .orderBy("commitId", "desc");
@@ -344,6 +346,20 @@ export const folderCommitDALFactory = (db: TDbClient) => {
} }
}; };
const findPreviousCommitTo = async (folderId: string, commitId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
try {
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ folderId })
.where("commitId", "<=", commitId)
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc")
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindPreviousCommitTo" });
}
};
return { return {
...restOfOrm, ...restOfOrm,
findByFolderId, findByFolderId,
@@ -356,6 +372,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
findLatestEnvCommit, findLatestEnvCommit,
getEnvNumberOfCommitsSince, getEnvNumberOfCommitsSince,
findLatestCommitByFolderIds, findLatestCommitByFolderIds,
findAllFolderCommitsAfter findAllFolderCommitsAfter,
findPreviousCommitTo
}; };
}; };
@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TSecretFolders } from "@app/db/schemas"; import { TSecretFolders } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -20,7 +22,7 @@ type TFolderCommitQueueServiceFactoryDep = {
>; >;
folderCommitDAL: Pick< folderCommitDAL: Pick<
TFolderCommitDALFactory, TFolderCommitDALFactory,
"findLatestEnvCommit" | "getEnvNumberOfCommitsSince" | "findMultipleLatestCommits" "findLatestEnvCommit" | "getEnvNumberOfCommitsSince" | "findMultipleLatestCommits" | "findById"
>; >;
folderDAL: Pick<TSecretFolderDALFactory, "findByEnvId">; folderDAL: Pick<TSecretFolderDALFactory, "findByEnvId">;
}; };
@@ -42,7 +44,7 @@ export const folderCommitQueueServiceFactory = ({
QueueJobs.CreateFolderTreeCheckpoint, QueueJobs.CreateFolderTreeCheckpoint,
{ envId }, { envId },
{ {
jobId: envId, jobId: `${envId}`,
backoff: { backoff: {
type: "exponential", type: "exponential",
delay: 3000 delay: 3000
@@ -119,52 +121,66 @@ export const folderCommitQueueServiceFactory = ({
return result; return result;
}; };
const createFolderTreeCheckpoint = async (envId: string, folderCommitId?: string, tx?: Knex) => {
logger.info("Folder tree checkpoint creation started:", envId);
const latestTreeCheckpoint = await folderTreeCheckpointDAL.findLatestByEnvId(envId, tx);
let latestCommit;
if (folderCommitId) {
latestCommit = await folderCommitDAL.findById(folderCommitId, tx);
} else {
latestCommit = await folderCommitDAL.findLatestEnvCommit(envId, tx);
}
if (!latestCommit) {
logger.info(`Latest commit ID not found for envId ${envId}`);
return;
}
const latestCommitId = latestCommit.id;
if (latestTreeCheckpoint) {
const commitsSinceLastCheckpoint = await folderCommitDAL.getEnvNumberOfCommitsSince(
envId,
latestTreeCheckpoint.folderCommitId,
tx
);
if (commitsSinceLastCheckpoint < Number(appCfg.PIT_TREE_CHECKPOINT_WINDOW)) {
logger.info(
`Commits since last checkpoint ${commitsSinceLastCheckpoint} is less than ${appCfg.PIT_TREE_CHECKPOINT_WINDOW}`
);
return;
}
}
const folders = await folderDAL.findByEnvId(envId, tx);
const sortedFolders = sortFoldersByHierarchy(folders);
const filteredFoldersIds = sortedFolders.filter((folder) => !folder.isReserved).map((folder) => folder.id);
const folderCommits = await folderCommitDAL.findMultipleLatestCommits(filteredFoldersIds, tx);
const folderTreeCheckpoint = await folderTreeCheckpointDAL.create(
{
folderCommitId: latestCommitId
},
tx
);
await folderTreeCheckpointResourcesDAL.insertMany(
folderCommits.map((folderCommit) => ({
folderTreeCheckpointId: folderTreeCheckpoint.id,
folderId: folderCommit.folderId,
folderCommitId: folderCommit.id
})),
tx
);
logger.info("Folder tree checkpoint created successfully:", folderTreeCheckpoint.id);
};
queueService.start(QueueName.FolderTreeCheckpoint, async (job) => { queueService.start(QueueName.FolderTreeCheckpoint, async (job) => {
try { try {
if (job.name === QueueJobs.CreateFolderTreeCheckpoint) { if (job.name === QueueJobs.CreateFolderTreeCheckpoint) {
const { envId } = job.data as { envId: string }; const { envId } = job.data as { envId: string };
logger.info("Folder tree checkpoint creation started:", envId, job.id); await createFolderTreeCheckpoint(envId);
const latestTreeCheckpoint = await folderTreeCheckpointDAL.findLatestByEnvId(envId);
const latestCommit = await folderCommitDAL.findLatestEnvCommit(envId);
if (!latestCommit) {
logger.info(`Latest commit ID not found for envId ${envId}`);
return;
}
const latestCommitId = latestCommit.id;
if (latestTreeCheckpoint) {
const commitsSinceLastCheckpoint = await folderCommitDAL.getEnvNumberOfCommitsSince(
envId,
latestTreeCheckpoint.folderCommitId
);
if (commitsSinceLastCheckpoint < Number(appCfg.PIT_TREE_CHECKPOINT_WINDOW)) {
logger.info(
`Commits since last checkpoint ${commitsSinceLastCheckpoint} is less than ${appCfg.PIT_TREE_CHECKPOINT_WINDOW}`
);
return;
}
}
const folders = await folderDAL.findByEnvId(envId);
const sortedFolders = sortFoldersByHierarchy(folders);
const filteredFoldersIds = sortedFolders.filter((folder) => !folder.isReserved).map((folder) => folder.id);
const folderCommits = await folderCommitDAL.findMultipleLatestCommits(filteredFoldersIds);
const folderTreeCheckpoint = await folderTreeCheckpointDAL.create({
folderCommitId: latestCommitId
});
await folderTreeCheckpointResourcesDAL.insertMany(
folderCommits.map((folderCommit) => ({
folderTreeCheckpointId: folderTreeCheckpoint.id,
folderId: folderCommit.folderId,
folderCommitId: folderCommit.id
}))
);
logger.info("Folder tree checkpoint created successfully:", folderTreeCheckpoint.id);
} }
} catch (error) { } catch (error) {
logger.error(error, "Error creating folder tree checkpoint:"); logger.error(error, "Error creating folder tree checkpoint:");
@@ -175,6 +191,7 @@ export const folderCommitQueueServiceFactory = ({
return { return {
scheduleTreeCheckpoint, scheduleTreeCheckpoint,
schedulePeriodicTreeCheckpoint, schedulePeriodicTreeCheckpoint,
cancelScheduledTreeCheckpoint cancelScheduledTreeCheckpoint,
createFolderTreeCheckpoint
}; };
}; };
@@ -131,7 +131,8 @@ describe("folderCommitServiceFactory", () => {
}; };
const mockFolderCommitQueueService = { const mockFolderCommitQueueService = {
scheduleTreeCheckpoint: vi.fn().mockResolvedValue({}) scheduleTreeCheckpoint: vi.fn().mockResolvedValue({}),
createFolderTreeCheckpoint: vi.fn().mockResolvedValue({})
}; };
const mockPermissionService = { const mockPermissionService = {
@@ -116,7 +116,10 @@ type TFolderCommitServiceFactoryDep = {
secretVersionV2BridgeDAL: TSecretVersionV2DALFactory; secretVersionV2BridgeDAL: TSecretVersionV2DALFactory;
secretV2BridgeDAL: secretV2BridgeDal.TSecretV2BridgeDALFactory; secretV2BridgeDAL: secretV2BridgeDal.TSecretV2BridgeDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
folderCommitQueueService?: Pick<TFolderCommitQueueServiceFactory, "scheduleTreeCheckpoint">; folderCommitQueueService?: Pick<
TFolderCommitQueueServiceFactory,
"scheduleTreeCheckpoint" | "createFolderTreeCheckpoint"
>;
permissionService?: TPermissionServiceFactory; permissionService?: TPermissionServiceFactory;
}; };
@@ -226,18 +229,16 @@ export const folderCommitServiceFactory = ({
const checkpointResources = await getFolderResources(folderId, tx); const checkpointResources = await getFolderResources(folderId, tx);
if (checkpointResources.length > 0) { const newCheckpoint = await folderCheckpointDAL.create(
const newCheckpoint = await folderCheckpointDAL.create( {
{ folderCommitId: latestCommitId
folderCommitId: latestCommitId },
}, tx
tx );
); await folderCheckpointResourcesDAL.insertMany(
await folderCheckpointResourcesDAL.insertMany( checkpointResources.map((resource) => ({ folderCheckpointId: newCheckpoint.id, ...resource })),
checkpointResources.map((resource) => ({ folderCheckpointId: newCheckpoint.id, ...resource })), tx
tx );
);
}
return latestCommitId; return latestCommitId;
}; };
@@ -357,10 +358,12 @@ export const folderCommitServiceFactory = ({
const compareFolderStates = async ({ const compareFolderStates = async ({
currentCommitId, currentCommitId,
targetCommitId, targetCommitId,
defaultOperation = "create",
tx tx
}: { }: {
currentCommitId?: string; currentCommitId?: string;
targetCommitId: string; targetCommitId: string;
defaultOperation?: "create" | "update" | "delete";
tx?: Knex; tx?: Knex;
}) => { }) => {
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx); const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
@@ -376,7 +379,7 @@ export const folderCommitServiceFactory = ({
type: resource.type, type: resource.type,
id: resource.id, id: resource.id,
versionId: resource.versionId, versionId: resource.versionId,
changeType: "create", changeType: defaultOperation,
commitId: targetCommit.commitId, commitId: targetCommit.commitId,
secretKey: resource.secretKey, secretKey: resource.secretKey,
secretVersion: resource.secretVersion, secretVersion: resource.secretVersion,
@@ -531,6 +534,40 @@ export const folderCommitServiceFactory = ({
throw new NotFoundError({ message: `Folder with ID ${data.folderId} not found` }); throw new NotFoundError({ message: `Folder with ID ${data.folderId} not found` });
} }
const newFolders = data.changes.filter(
(change) => change.type === "add" && !change.isUpdate && change.folderVersionId
);
if (newFolders.length > 0) {
const folderVersions = await folderVersionDAL.find(
{
$in: {
id: newFolders.map((change) => change.folderVersionId).filter(Boolean) as string[]
}
},
{ tx }
);
await Promise.all(
Object.values(folderVersions).map(async (folderVersion) => {
const subFolderCommit = await folderCommitDAL.create(
{
actorMetadata: metadata,
actorType: data.actor.type,
message: data.message,
folderId: folderVersion.folderId,
envId: folderVersion.envId
},
tx
);
await createFolderCheckpoint({
folderId: folderVersion.folderId,
folderCommitId: subFolderCommit.id,
force: true,
tx
});
})
);
}
const newCommit = await folderCommitDAL.create( const newCommit = await folderCommitDAL.create(
{ {
actorMetadata: metadata, actorMetadata: metadata,
@@ -553,8 +590,14 @@ export const folderCommitServiceFactory = ({
tx tx
); );
await createFolderCheckpoint({ folderId: data.folderId, tx }); await createFolderCheckpoint({ folderId: data.folderId, folderCommitId: newCommit.id, tx });
if (folderCommitQueueService) { if (folderCommitQueueService) {
if (!folder.parentId) {
const previousTreeCommit = await folderTreeCheckpointDAL.findLatestByEnvId(folder.envId);
if (!previousTreeCommit) {
await folderCommitQueueService.createFolderTreeCheckpoint(folder.envId, newCommit.id, tx);
}
}
await folderCommitQueueService.scheduleTreeCheckpoint(folder.envId); await folderCommitQueueService.scheduleTreeCheckpoint(folder.envId);
} }
return newCommit; return newCommit;
@@ -1219,30 +1262,31 @@ export const folderCommitServiceFactory = ({
const folderCheckpointCommits = await folderTreeCheckpointResourcesDAL.findByTreeCheckpointId(checkpoint.id, tx); const folderCheckpointCommits = await folderTreeCheckpointResourcesDAL.findByTreeCheckpointId(checkpoint.id, tx);
const folderCommits = await folderCommitDAL.findAllCommitsBetween({ const folderCommits = await folderCommitDAL.findAllCommitsBetween({
envId, envId,
endCommitId: targetCommit.commitId.toString(),
startCommitId: checkpoint.commitId.toString(), startCommitId: checkpoint.commitId.toString(),
tx tx
}); });
// Group commits by folderId and keep only the latest // Group commits by folderId and keep only the latest
const folderGroups = new Map<string, { createdAt: Date; id: string }>(); const folderGroups = new Map<string, { commitId: number; id: string }>();
if (folderCheckpointCommits && folderCheckpointCommits.length > 0) { if (folderCheckpointCommits && folderCheckpointCommits.length > 0) {
for (const commit of folderCheckpointCommits) { for (const commit of folderCheckpointCommits) {
folderGroups.set(commit.folderId, { if (commit.commitId > targetCommit.commitId) {
createdAt: commit.createdAt, folderGroups.set(commit.folderId, {
id: commit.folderCommitId commitId: commit.commitId,
}); id: commit.folderCommitId
});
}
} }
} }
if (folderCommits && folderCommits.length > 0) { if (folderCommits && folderCommits.length > 0) {
for (const commit of folderCommits) { for (const commit of folderCommits) {
const { folderId, createdAt, id } = commit; const { folderId, commitId, id } = commit;
const existingCommit = folderGroups.get(folderId); const existingCommit = folderGroups.get(folderId);
if (!existingCommit || createdAt.getTime() > existingCommit.createdAt.getTime()) { if ((!existingCommit || commitId > existingCommit.commitId) && commitId > targetCommit.commitId) {
folderGroups.set(folderId, { createdAt, id }); folderGroups.set(folderId, { commitId, id });
} }
} }
} }
@@ -1252,16 +1296,27 @@ export const folderCommitServiceFactory = ({
// Process each folder to determine differences // Process each folder to determine differences
await Promise.all( await Promise.all(
Array.from(folderGroups.entries()).map(async ([folderId, commit]) => { Array.from(folderGroups.entries()).map(async ([folderId, commit]) => {
const latestFolderCommit = await folderCommitDAL.findLatestCommit(folderId, tx); const previousCommit = await folderCommitDAL.findPreviousCommitTo(
if (latestFolderCommit && latestFolderCommit.id !== commit.id) { folderId,
const diff = await compareFolderStates({ targetCommit.commitId.toString(),
currentCommitId: latestFolderCommit.id, tx
targetCommitId: commit.id, );
let diff = [];
if (previousCommit && previousCommit.id !== commit.id) {
diff = await compareFolderStates({
currentCommitId: commit.id,
targetCommitId: previousCommit.id,
tx tx
}); });
if (diff?.length > 0) { } else {
folderDiffs.set(folderId, diff); diff = await compareFolderStates({
} targetCommitId: commit.id,
defaultOperation: "delete",
tx
});
}
if (diff?.length > 0) {
folderDiffs.set(folderId, diff);
} }
}) })
); );
@@ -1295,92 +1350,115 @@ export const folderCommitServiceFactory = ({
envId: string, envId: string,
actorId: string, actorId: string,
actorType: ActorType, actorType: ActorType,
projectId: string, projectId: string
tx?: Knex
) => { ) => {
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx); await folderCommitDAL.transaction(async (tx) => {
if (!targetCommit) { const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
throw new NotFoundError({ message: `No commit found for commit ID ${targetCommitId}` }); if (!targetCommit) {
} throw new NotFoundError({ message: `No commit found for commit ID ${targetCommitId}` });
const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommitId, envId, tx);
if (!checkpoint) {
throw new NotFoundError({ message: `No checkpoint found for commit ID ${targetCommitId}` });
}
const folderCheckpointCommits = await folderTreeCheckpointResourcesDAL.findByTreeCheckpointId(checkpoint.id, tx);
const folderCommits = await folderCommitDAL.findAllCommitsBetween({
envId,
endCommitId: targetCommit.commitId.toString(),
startCommitId: checkpoint.commitId.toString(),
tx
});
// Group commits by folderId and keep only the latest
const folderGroups = new Map<string, { createdAt: Date; id: string }>();
if (folderCheckpointCommits && folderCheckpointCommits.length > 0) {
for (const commit of folderCheckpointCommits) {
folderGroups.set(commit.folderId, {
createdAt: commit.createdAt,
id: commit.folderCommitId
});
} }
}
if (folderCommits && folderCommits.length > 0) { const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommitId, envId, tx);
for (const commit of folderCommits) { if (!checkpoint) {
const { folderId, createdAt, id } = commit; throw new NotFoundError({ message: `No checkpoint found for commit ID ${targetCommitId}` });
const existingCommit = folderGroups.get(folderId);
if (!existingCommit || createdAt.getTime() > existingCommit.createdAt.getTime()) {
folderGroups.set(folderId, { createdAt, id });
}
} }
}
const folderDiffs = new Map<string, ResourceChange[]>(); const folderCheckpointCommits = await folderTreeCheckpointResourcesDAL.findByTreeCheckpointId(checkpoint.id, tx);
const folderCommits = await folderCommitDAL.findAllCommitsBetween({
envId,
startCommitId: checkpoint.commitId.toString(),
tx
});
// Process each folder to determine differences // Group commits by folderId and keep only the latest
await Promise.all( const folderGroups = new Map<string, { commitId: number; id: string }>();
Array.from(folderGroups.entries()).map(async ([folderId, commit]) => {
const latestFolderCommit = await folderCommitDAL.findLatestCommit(folderId, tx); if (folderCheckpointCommits && folderCheckpointCommits.length > 0) {
if (latestFolderCommit && latestFolderCommit.id !== commit.id) { for (const commit of folderCheckpointCommits) {
const diff = await compareFolderStates({ if (commit.commitId > targetCommit.commitId) {
currentCommitId: latestFolderCommit.id, folderGroups.set(commit.folderId, {
targetCommitId: commit.id, commitId: commit.commitId,
tx id: commit.folderCommitId
}); });
if (diff?.length > 0) {
folderDiffs.set(folderId, diff);
} }
} }
})
);
// Apply changes in hierarchical order
const folderIds = Array.from(folderDiffs.keys());
const folders = await folderDAL.findFoldersByRootAndIds({ rootId: targetCommit.folderId, folderIds }, tx);
const sortedFolders = sortFoldersByHierarchy(folders);
for (const folder of sortedFolders) {
const diff = folderDiffs.get(folder.id);
if (diff) {
await applyFolderStateDifferences({
differences: diff,
actorInfo: {
actorType,
actorId,
message: "Deep rollback"
},
folderId: folder.id,
projectId,
reconstructNewFolders: true,
reconstructUpToCommit: targetCommit.commitId.toString(),
tx
});
} }
}
if (folderCommits && folderCommits.length > 0) {
for (const commit of folderCommits) {
const { folderId, commitId, id } = commit;
const existingCommit = folderGroups.get(folderId);
if ((!existingCommit || commitId > existingCommit.commitId) && commitId > targetCommit.commitId) {
folderGroups.set(folderId, { commitId, id });
}
}
}
const folderDiffs = new Map<string, ResourceChange[]>();
// Process each folder to determine differences
await Promise.all(
Array.from(folderGroups.entries()).map(async ([folderId, { id }]) => {
const previousCommit = await folderCommitDAL.findPreviousCommitTo(
folderId,
targetCommit.commitId.toString(),
tx
);
if (previousCommit && previousCommit.id !== id) {
const diff = await compareFolderStates({
currentCommitId: id,
targetCommitId: previousCommit.id,
tx
});
if (diff?.length > 0) {
folderDiffs.set(folderId, diff);
}
}
})
);
const foldersToDelete = new Set<string>();
// Process all DELETE operations to build a complete set of folders to be deleted
for (const [folderId, changes] of folderDiffs.entries()) {
for (const change of changes) {
if (change.changeType === ChangeType.DELETE && change.type === "folder") {
foldersToDelete.add(change.id);
}
}
}
// Now, remove any folder that is being deleted from the folderDiffs map
// before applying any changes
for (const folderId of foldersToDelete) {
folderDiffs.delete(folderId);
}
// Apply changes in hierarchical order
const folderIds = Array.from(folderDiffs.keys());
const folders = await folderDAL.findFoldersByRootAndIds({ rootId: targetCommit.folderId, folderIds }, tx);
const sortedFolders = sortFoldersByHierarchy(folders);
for (const folder of sortedFolders) {
const diff = folderDiffs.get(folder.id);
if (diff) {
await applyFolderStateDifferences({
differences: diff,
actorInfo: {
actorType,
actorId,
message: "Deep rollback"
},
folderId: folder.id,
projectId,
reconstructNewFolders: true,
reconstructUpToCommit: targetCommit.commitId.toString(),
tx
});
}
}
});
}; };
const getLatestCommit = async ({ const getLatestCommit = async ({
@@ -34,7 +34,7 @@ export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
try { try {
const targetCommit = await (tx || db.replicaNode())(TableName.FolderCommit) const targetCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ id: folderCommitId }) .where({ id: folderCommitId })
.select("id", "commitId", "folderId") .select("id", "commitId", "folderId", "envId")
.first(); .first();
if (!targetCommit) { if (!targetCommit) {
@@ -42,13 +42,12 @@ export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
} }
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderTreeCheckpoint) const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderTreeCheckpoint)
.join<TFolderCommits>( .leftJoin<TFolderCommits>(
TableName.FolderCommit, TableName.FolderCommit,
`${TableName.FolderTreeCheckpoint}.folderCommitId`, `${TableName.FolderTreeCheckpoint}.folderCommitId`,
`${TableName.FolderCommit}.id` `${TableName.FolderCommit}.id`
) )
.where(`${TableName.FolderCommit}.commitId`, "<=", targetCommit.commitId.toString()) .where(`${TableName.FolderCommit}.envId`, "=", targetCommit.envId)
.where(`${TableName.FolderCommit}.envId`, envId)
.select(selectAllTableCols(TableName.FolderTreeCheckpoint)) .select(selectAllTableCols(TableName.FolderTreeCheckpoint))
.select(db.ref("commitId").withSchema(TableName.FolderCommit)) .select(db.ref("commitId").withSchema(TableName.FolderCommit))
.orderBy(`${TableName.FolderCommit}.commitId`, "desc") .orderBy(`${TableName.FolderCommit}.commitId`, "desc")
@@ -206,32 +206,45 @@ export const RollbackPreviewTab = (): JSX.Element => {
{currentFolderChanges.folderPath || currentFolderChanges.folderName} {currentFolderChanges.folderPath || currentFolderChanges.folderName}
</span> </span>
</div> </div>
{currentFolderChanges.changes.length > 0 && (
<span className="ml-2 rounded-full bg-mineshaft-600 px-2 py-0.5 text-xs text-gray-300">
{currentFolderChanges.changes.length}
</span>
)}
</div> </div>
</div> </div>
{deepRollback && nestedFolderChanges.length > 0 && ( {deepRollback && nestedFolderChanges.length > 0 && (
<> <>
<div className="border-b border-mineshaft-600 bg-mineshaft-800 px-4 py-2"> <div className="border-b border-mineshaft-600 bg-mineshaft-800 px-4 py-2">
<span className="text-sm font-semibold text-white">Affected Child Folders</span> <span className="text-sm font-semibold text-white">
Affected Child Folders
</span>
</div> </div>
{nestedFolderChanges.map((folder) => ( {(nestedFolderChanges.map((folder) => (
<div <div
key={folder.folderId} key={folder.folderId}
className={`cursor-pointer border-b border-mineshaft-600 ${ className={`cursor-pointer border-b border-mineshaft-600 ${
selectedFolderId === folder.folderId ? "bg-mineshaft-700" : "" selectedFolderId === folder.folderId ? "bg-mineshaft-700" : ""
}`} }`}
onClick={() => setSelectedFolderId(folder.folderId)} onClick={() => setSelectedFolderId(folder.folderId)}
> >
<div className="flex items-center justify-between px-4 py-2"> <div className="flex items-center justify-between px-4 py-2">
<div className="flex items-center"> <div className="flex items-center">
<FontAwesomeIcon icon={faFolder} className="mr-2 text-yellow-500" size="sm" /> <FontAwesomeIcon icon={faFolder} className="mr-2 text-yellow-500" size="sm" />
<span className="max-w-[150px] truncate text-sm font-medium text-white"> <span className="max-w-[150px] truncate text-sm font-medium text-white">
{folder.folderPath || folder.folderName} {folder.folderPath || folder.folderName}
</span> </span>
</div>
{folder.changes.length > 0 && (
<span className="rounded-full bg-mineshaft-600 px-2 py-0.5 text-xs text-gray-300">
{folder.changes.length}
</span>
)}
</div> </div>
</div> </div>
</div> ))
))} )}
</> </>
)} )}
</div> </div>
@@ -292,16 +305,10 @@ export const RollbackPreviewTab = (): JSX.Element => {
description="This will restore all changes to how they appeared at the point in time of this commit. Any modifications made after this commit will be undone." description="This will restore all changes to how they appeared at the point in time of this commit. Any modifications made after this commit will be undone."
/> />
{folderChanges.length > 0 ? ( <div className="flex w-full border border-mineshaft-600">
<div className="flex w-full border border-mineshaft-600">
{renderSidebar()} {renderSidebar()}
{renderMainContent()} {renderMainContent()}
</div> </div>
) : (
<div className="flex h-32 items-center justify-center">
<p className="text-gray-400">No changes will be applied with this restore</p>
</div>
)}
<div className="border-x border-mineshaft-600 bg-mineshaft-800 px-6 py-3"> <div className="border-x border-mineshaft-600 bg-mineshaft-800 px-6 py-3">
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
@@ -346,6 +353,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
}} }}
colorSchema="primary" colorSchema="primary"
className="px-6 py-2" className="px-6 py-2"
isDisabled={message.length === 0 || !rollbackChangesNested || !rollbackChangesNested?.some((folder) => folder.changes.length > 0)}
> >
Restore Restore
</Button> </Button>