mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 19:28:16 +00:00
PIT: fix blocker for deep rollbacks
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
+32
-24
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user