Add PIT rollback

This commit is contained in:
carlosmonastyrski
2025-05-09 16:03:50 -03:00
parent 78d5bc823d
commit 9e1d38a27b
7 changed files with 484 additions and 12 deletions
@@ -14,6 +14,7 @@ import { TIdentityDALFactory } from "../identity/identity-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TFolderCommitDALFactory } from "./folder-commit-dal";
@@ -38,14 +39,33 @@ type TFolderCommitServiceFactoryDep = {
folderTreeCheckpointDAL: Pick<TFolderTreeCheckpointDALFactory, "findByProjectId" | "findLatestByProjectId">;
userDAL: Pick<TUserDALFactory, "findById">;
identityDAL: Pick<TIdentityDALFactory, "findById">;
folderDAL: Pick<TSecretFolderDALFactory, "findByParentId" | "findByProjectId">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findByParentId" | "findByProjectId" | "deleteById" | "create" | "updateById" | "update"
>;
folderVersionDAL: Pick<
TSecretFolderVersionDALFactory,
"findLatestFolderVersions" | "findById" | "deleteById" | "create" | "updateById"
| "findLatestFolderVersions"
| "findById"
| "deleteById"
| "create"
| "updateById"
| "find"
| "findByIdsWithLatestVersion"
>;
secretVersionV2BridgeDAL: Pick<
TSecretVersionV2DALFactory,
"findLatestVersionByFolderId" | "findById" | "deleteById" | "create" | "updateById"
| "findLatestVersionByFolderId"
| "findById"
| "deleteById"
| "create"
| "updateById"
| "find"
| "findByIdsWithLatestVersion"
>;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"deleteById" | "create" | "updateById" | "update" | "insertMany" | "invalidateSecretCacheByProjectId"
>;
projectDAL: Pick<TProjectDALFactory, "findById">;
};
@@ -85,7 +105,8 @@ export const folderCommitServiceFactory = ({
folderDAL,
folderVersionDAL,
secretVersionV2BridgeDAL,
projectDAL
projectDAL,
secretV2BridgeDAL
}: TFolderCommitServiceFactoryDep) => {
const appCfg = getConfig();
@@ -356,6 +377,261 @@ export const folderCommitServiceFactory = ({
}
};
const applyFolderStateDifferences = async (
differences: Array<{
type: string;
id: string;
versionId: string;
oldVersionId?: string;
changeType: "create" | "update" | "delete";
}>,
actorInfo: {
actorType: string;
actorId?: string;
message?: string;
},
folderId: string,
projectId: string
) => {
let result = {};
await folderCommitDAL.transaction(async (tx) => {
// Group differences by type for more efficient processing
const secretChanges = differences.filter((diff) => diff.type === "secret");
const folderChanges = differences.filter((diff) => diff.type === "folder");
const secretVersions = await secretVersionV2BridgeDAL.findByIdsWithLatestVersion(
folderId,
secretChanges.map((diff) => diff.id),
secretChanges.map((diff) => diff.versionId)
);
const folderVersions = await folderVersionDAL.findByIdsWithLatestVersion(
folderChanges.map((diff) => diff.id),
folderChanges.map((diff) => diff.versionId)
);
// Track all changes for commit recording
const commitChanges = [];
// Process secret changes
for (const change of secretChanges) {
const secretVersion = secretVersions[change.id];
switch (change.changeType) {
case "create":
if (secretVersion) {
const newSecret = [
{
id: change.id,
skipMultilineEncoding: secretVersion.skipMultilineEncoding,
version: secretVersion.version + 1,
type: secretVersion.type,
key: secretVersion.key,
reminderNote: secretVersion.reminderNote,
reminderRepeatDays: secretVersion.reminderRepeatDays,
encryptedValue: secretVersion.encryptedValue,
encryptedComment: secretVersion.encryptedComment,
userId: secretVersion.userId,
metadata: secretVersion.metadata,
folderId
}
];
await secretV2BridgeDAL.insertMany(newSecret, tx);
const newVersion = await secretVersionV2BridgeDAL.create(
{
folderId,
secretId: secretVersion.secretId,
version: secretVersion.version + 1,
encryptedValue: secretVersion.encryptedValue,
key: secretVersion.key,
encryptedComment: secretVersion.encryptedComment,
skipMultilineEncoding: secretVersion.skipMultilineEncoding,
reminderNote: secretVersion.reminderNote,
reminderRepeatDays: secretVersion.reminderRepeatDays,
userId: secretVersion.userId,
metadata: secretVersion.metadata,
actorType: actorInfo.actorType,
envId: secretVersion.envId,
...(actorInfo.actorType === ActorType.IDENTITY && { identityActorId: actorInfo.actorId }),
...(actorInfo.actorType === ActorType.USER && { userActorId: actorInfo.actorId })
},
tx
);
commitChanges.push({
type: "add",
secretVersionId: newVersion.id
});
}
break;
case "update":
// Update secret to specific version
if (secretVersion) {
await secretV2BridgeDAL.updateById(
change.id,
{
skipMultilineEncoding: secretVersion?.skipMultilineEncoding,
version: secretVersion?.version,
type: secretVersion?.type,
key: secretVersion?.key,
reminderNote: secretVersion?.reminderNote,
reminderRepeatDays: secretVersion?.reminderRepeatDays,
encryptedValue: secretVersion?.encryptedValue,
encryptedComment: secretVersion?.encryptedComment,
userId: secretVersion?.userId,
metadata: secretVersion?.metadata
},
tx
);
const newVersion = await secretVersionV2BridgeDAL.create(
{
version: secretVersion.version + 1,
encryptedValue: secretVersion.encryptedValue,
key: secretVersion.key,
encryptedComment: secretVersion.encryptedComment,
skipMultilineEncoding: secretVersion.skipMultilineEncoding,
reminderNote: secretVersion.reminderNote,
reminderRepeatDays: secretVersion.reminderRepeatDays,
userId: secretVersion.userId,
metadata: secretVersion.metadata,
actorType: actorInfo.actorType,
envId: secretVersion.envId,
folderId,
secretId: secretVersion.secretId,
...(actorInfo.actorType === ActorType.IDENTITY && { identityActorId: actorInfo.actorId }),
...(actorInfo.actorType === ActorType.USER && { userActorId: actorInfo.actorId })
},
tx
);
commitChanges.push({
type: "add",
secretVersionId: newVersion.id
});
}
break;
case "delete":
await secretV2BridgeDAL.deleteById(change.id, tx);
commitChanges.push({
type: "delete",
secretVersionId: change.versionId
});
break;
default:
throw new BadRequestError({ message: `Unknown change type: ${change.changeType as string}` });
}
}
// process folder changes
for (const change of folderChanges) {
const folderVersion = folderVersions[change.id];
switch (change.changeType) {
case "create":
// Add new folder
if (folderVersion) {
const newFolder = {
id: change.id,
parentId: folderId,
envId: folderVersion.envId,
version: (folderVersion.version || 1) + 1,
name: folderVersion.name
};
await folderDAL.create(newFolder, tx);
const newFolderVersion = await folderVersionDAL.create(
{
folderId: change.id,
version: (folderVersion.version || 1) + 1,
name: folderVersion.name,
envId: folderVersion.envId
},
tx
);
commitChanges.push({
type: "add",
folderVersionId: newFolderVersion.id
});
}
break;
case "update":
// Update folder to specific version
if (change.versionId) {
await folderVersionDAL.findById(change.versionId, tx).then(async (versionDetails) => {
if (versionDetails) {
await folderDAL.updateById(
change.id,
{
parentId: folderId,
envId: versionDetails.envId,
version: (versionDetails.version || 1) + 1,
name: versionDetails.name
},
tx
);
const newFolderVersion = await folderVersionDAL.create(
{
folderId: change.id,
version: (versionDetails.version || 1) + 1,
name: versionDetails.name,
envId: versionDetails.envId
},
tx
);
commitChanges.push({
type: "add",
folderVersionId: newFolderVersion.id
});
}
});
}
break;
case "delete":
await folderDAL.deleteById(change.id, tx);
commitChanges.push({
type: "delete",
folderVersionId: change.versionId
});
break;
default:
throw new BadRequestError({ message: `Unknown change type: ${change.changeType as string}` });
}
}
await createCommit(
{
actor: {
type: actorInfo.actorType,
metadata: { id: actorInfo.actorId }
},
message: actorInfo.message || "Rolled back folder state",
folderId,
changes: commitChanges
},
tx
);
result = {
secretChangesCount: secretChanges.length,
folderChangesCount: folderChanges.length,
totalChanges: differences.length
};
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
});
return result;
};
// Retrieve a commit by ID
const getCommitById = async (id: string, tx?: Knex) => {
return folderCommitDAL.findById(id, tx);
@@ -467,7 +743,8 @@ export const folderCommitServiceFactory = ({
initializeFolder,
initializeProject,
createFolderCheckpoint,
compareFolderStates
compareFolderStates,
applyFolderStateDifferences
};
};
@@ -96,5 +96,80 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
};
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };
const findByIdsWithLatestVersion = async (folderIds: string[], versionIds?: string[], tx?: Knex) => {
try {
if (!folderIds.length && (!versionIds || !versionIds.length)) return {};
const knexInstance = tx || db.replicaNode();
let allDocs: Array<TSecretFolderVersions & { max?: number }> = [];
// Process latest versions for folderIds
if (folderIds.length) {
const latestVersions: Array<TSecretFolderVersions & { max: number }> = await knexInstance(
TableName.SecretFolderVersion
)
.whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds)
.join(
knexInstance(TableName.SecretFolderVersion)
.groupBy("folderId")
.max("version")
.select("folderId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretFolderVersion}.folderId`, "latestVersion.folderId").andOn(
`${TableName.SecretFolderVersion}.version`,
"latestVersion.max"
);
}
);
allDocs = [...allDocs, ...latestVersions];
}
// Process specific versions by versionIds
if (versionIds && versionIds.length) {
// Get the specific versions
const specificVersions = await knexInstance(TableName.SecretFolderVersion).whereIn("id", versionIds);
// Get the folderIds from these versions
const specificFolderIds = [...new Set(specificVersions.map((v) => v.folderId).filter(Boolean))];
// Get max versions for these folderIds
const maxVersionsQuery = (await knexInstance(TableName.SecretFolderVersion)
.whereIn("folderId", specificFolderIds)
.groupBy("folderId")
.select("folderId")
.max("version as max")) as Array<{ folderId: string; max: number }>;
// Create a lookup map for max versions
const maxVersionMap = maxVersionsQuery.reduce<Record<string, number>>((acc, item) => {
acc[item.folderId] = item.max;
return acc;
}, {});
// Add max version to each specific version
const specificVersionsWithMax = specificVersions.map((version) => ({
...version,
max: maxVersionMap[version.folderId]
}));
allDocs = [...allDocs, ...specificVersionsWithMax];
}
return allDocs.reduce<Record<string, TSecretFolderVersions & { max?: number }>>(
(prev, curr) => ({ ...prev, [curr.folderId || ""]: curr }),
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
}
};
return {
...secretFolderVerOrm,
findLatestFolderVersions,
findLatestVersionByFolderId,
pruneExcessVersions,
findByIdsWithLatestVersion
};
};
@@ -138,7 +138,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
throw new DatabaseError({ error, name: "FindLatestVersionMany" });
}
};
@@ -261,6 +261,84 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
}
};
const findByIdsWithLatestVersion = async (
folderId: string,
secretIds: string[],
versionIds?: string[],
tx?: Knex
) => {
try {
if (!secretIds.length && (!versionIds || !versionIds.length)) return {};
const knexInstance = tx || db.replicaNode();
let allDocs: Array<TSecretVersionsV2 & { max?: number }> = [];
// Process latest versions for secretIds
if (secretIds.length) {
const latestVersions: Array<TSecretVersionsV2 & { max: number }> = await knexInstance(TableName.SecretVersionV2)
.where("folderId", folderId)
.whereIn(`${TableName.SecretVersionV2}.secretId`, secretIds)
.join(
knexInstance(TableName.SecretVersionV2)
.groupBy("secretId")
.max("version")
.select("secretId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretVersionV2}.secretId`, "latestVersion.secretId").andOn(
`${TableName.SecretVersionV2}.version`,
"latestVersion.max"
);
}
);
allDocs = [...allDocs, ...latestVersions];
}
// Process specific versions by versionIds
if (versionIds && versionIds.length) {
// Get the specific versions
const specificVersions = await knexInstance(TableName.SecretVersionV2)
.where("folderId", folderId)
.whereIn("id", versionIds);
// Get the secretIds from these versions
const specificSecretIds = [...new Set(specificVersions.map((v) => v.secretId).filter(Boolean))];
// Get max versions for these secretIds
const maxVersionsQuery = (await knexInstance(TableName.SecretVersionV2)
.whereIn("secretId", specificSecretIds)
.groupBy("secretId")
.select("secretId")
.max("version as max")) as Array<{ secretId: string; max: number }>;
// Create a lookup map for max versions
const maxVersionMap = maxVersionsQuery.reduce(
(acc, item) => {
acc[item.secretId] = item.max;
return acc;
},
{} as Record<string, number>
);
// Add max version to each specific version
const specificVersionsWithMax = specificVersions.map((version) => ({
...version,
max: maxVersionMap[version.secretId]
}));
allDocs = [...allDocs, ...specificVersionsWithMax];
}
return allDocs.reduce<Record<string, TSecretVersionsV2 & { max?: number }>>(
(prev, curr) => ({ ...prev, [curr.secretId || ""]: curr }),
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
}
};
return {
...secretVersionV2Orm,
pruneExcessVersions,
@@ -268,6 +346,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
bulkUpdate,
findLatestVersionByFolderId,
findVersionsBySecretIdWithActors,
findBySecretId
findBySecretId,
findByIdsWithLatestVersion
};
};