mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 08:29:55 +00:00
PIT: address PR suggestions
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
TableName,
|
||||
TFolderCommitChanges,
|
||||
TFolderCommits,
|
||||
TProjectEnvironments,
|
||||
TSecretFolderVersions,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
@@ -54,10 +55,20 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findLatestCommit = async (folderId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
|
||||
const findLatestCommit = async (
|
||||
folderId: string,
|
||||
projectId?: string,
|
||||
tx?: Knex
|
||||
): Promise<TFolderCommits | undefined> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.where({ folderId })
|
||||
.leftJoin(TableName.Environment, `${TableName.FolderCommit}.envId`, `${TableName.Environment}.id`)
|
||||
.where((qb) => {
|
||||
if (projectId) {
|
||||
void qb.where(`${TableName.Environment}.projectId`, "=", projectId);
|
||||
}
|
||||
})
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy("commitId", "desc")
|
||||
.first();
|
||||
@@ -178,8 +189,8 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findCommitsToRecreate = async (
|
||||
folderId: string,
|
||||
targetCommitNumber: number,
|
||||
checkpointCommitNumber: number,
|
||||
targetCommitNumber: bigint,
|
||||
checkpointCommitNumber: bigint,
|
||||
tx?: Knex
|
||||
): Promise<
|
||||
(TFolderCommits & {
|
||||
@@ -213,7 +224,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
|
||||
// Get all changes for these commits in a single query
|
||||
const allChanges = await (tx || db.replicaNode())(TableName.FolderCommitChanges)
|
||||
.whereIn("folderCommitId", commitIds)
|
||||
.whereIn(`${TableName.FolderCommitChanges}.folderCommitId`, commitIds)
|
||||
.leftJoin<TSecretVersionsV2>(
|
||||
TableName.SecretVersionV2,
|
||||
`${TableName.FolderCommitChanges}.secretVersionId`,
|
||||
@@ -291,7 +302,6 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
endCommitId,
|
||||
tx
|
||||
}: {
|
||||
folderId?: string;
|
||||
envId?: string;
|
||||
startCommitId?: string;
|
||||
endCommitId?: string;
|
||||
@@ -323,7 +333,6 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
startCommitId,
|
||||
tx
|
||||
}: {
|
||||
folderId?: string;
|
||||
envId?: string;
|
||||
startCommitId?: string;
|
||||
tx?: Knex;
|
||||
@@ -346,7 +355,11 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findPreviousCommitTo = async (folderId: string, commitId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
|
||||
const findPreviousCommitTo = async (
|
||||
folderId: string,
|
||||
commitId: string,
|
||||
tx?: Knex
|
||||
): Promise<TFolderCommits | undefined> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.where({ folderId })
|
||||
@@ -360,6 +373,30 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex, projectId?: string): Promise<TFolderCommits> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ id }, TableName.FolderCommit))
|
||||
.leftJoin<TProjectEnvironments>(
|
||||
TableName.Environment,
|
||||
`${TableName.FolderCommit}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.where((qb) => {
|
||||
if (projectId) {
|
||||
void qb.where(`${TableName.Environment}.projectId`, "=", projectId);
|
||||
}
|
||||
})
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy("commitId", "desc")
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindById" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...restOfOrm,
|
||||
findByFolderId,
|
||||
@@ -373,6 +410,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
getEnvNumberOfCommitsSince,
|
||||
findLatestCommitByFolderIds,
|
||||
findAllFolderCommitsAfter,
|
||||
findPreviousCommitTo
|
||||
findPreviousCommitTo,
|
||||
findById
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TSecretFolders } from "@app/db/schemas";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
@@ -10,8 +11,16 @@ import { TFolderTreeCheckpointResourcesDALFactory } from "../folder-tree-checkpo
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TFolderCommitDALFactory } from "./folder-commit-dal";
|
||||
|
||||
// Define types for job data
|
||||
type TCreateFolderTreeCheckpointDTO = {
|
||||
envId: string;
|
||||
failedToAcquireLockCount?: number;
|
||||
folderCommitId?: string;
|
||||
};
|
||||
|
||||
type TFolderCommitQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem" | "deleteItem">;
|
||||
folderTreeCheckpointDAL: Pick<
|
||||
TFolderTreeCheckpointDALFactory,
|
||||
"create" | "findLatestByEnvId" | "findNearestCheckpoint"
|
||||
@@ -31,6 +40,7 @@ export type TFolderCommitQueueServiceFactory = ReturnType<typeof folderCommitQue
|
||||
|
||||
export const folderCommitQueueServiceFactory = ({
|
||||
queueService,
|
||||
keyStore,
|
||||
folderTreeCheckpointDAL,
|
||||
folderTreeCheckpointResourcesDAL,
|
||||
folderCommitDAL,
|
||||
@@ -38,48 +48,38 @@ export const folderCommitQueueServiceFactory = ({
|
||||
}: TFolderCommitQueueServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const scheduleTreeCheckpoint = async (envId: string) => {
|
||||
await queueService.queue(
|
||||
QueueName.FolderTreeCheckpoint,
|
||||
QueueJobs.CreateFolderTreeCheckpoint,
|
||||
{ envId },
|
||||
{
|
||||
jobId: `${envId}`,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
},
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
removeOnComplete: true
|
||||
}
|
||||
);
|
||||
// Helper function to calculate delay for requeuing
|
||||
const getRequeueDelay = (failureCount?: number) => {
|
||||
if (!failureCount) return 0;
|
||||
|
||||
const baseDelay = 5000;
|
||||
const maxDelay = 30000;
|
||||
|
||||
const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
|
||||
const jitter = delay * (0.5 + Math.random() * 0.5);
|
||||
|
||||
return jitter;
|
||||
};
|
||||
|
||||
const schedulePeriodicTreeCheckpoint = async (envId: string, intervalMs: number) => {
|
||||
await queueService.queue(
|
||||
QueueName.FolderTreeCheckpoint,
|
||||
QueueJobs.CreateFolderTreeCheckpoint,
|
||||
{ envId },
|
||||
{
|
||||
jobId: `periodic-${envId}`,
|
||||
repeat: {
|
||||
every: intervalMs
|
||||
},
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
},
|
||||
removeOnFail: false,
|
||||
removeOnComplete: false
|
||||
}
|
||||
);
|
||||
};
|
||||
const scheduleTreeCheckpoint = async (payload: TCreateFolderTreeCheckpointDTO) => {
|
||||
const { envId, failedToAcquireLockCount = 0 } = payload;
|
||||
|
||||
const cancelScheduledTreeCheckpoint = async (envId: string) => {
|
||||
await queueService.stopJobById(QueueName.FolderTreeCheckpoint, envId);
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.FolderTreeCheckpoint, `periodic-${envId}`);
|
||||
// Create a unique jobId for each retry to prevent conflicts
|
||||
const jobId =
|
||||
failedToAcquireLockCount > 0 ? `${envId}-retry-${failedToAcquireLockCount}-${Date.now()}` : `${envId}`;
|
||||
|
||||
await queueService.queue(QueueName.FolderTreeCheckpoint, QueueJobs.CreateFolderTreeCheckpoint, payload, {
|
||||
jobId,
|
||||
delay: getRequeueDelay(failedToAcquireLockCount),
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
},
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
removeOnComplete: true
|
||||
});
|
||||
};
|
||||
|
||||
// Sort folders by hierarchy (copied from the source code)
|
||||
@@ -121,66 +121,152 @@ export const folderCommitQueueServiceFactory = ({
|
||||
return result;
|
||||
};
|
||||
|
||||
const createFolderTreeCheckpoint = async (envId: string, folderCommitId?: string, tx?: Knex) => {
|
||||
logger.info("Folder tree checkpoint creation started:", envId);
|
||||
const createFolderTreeCheckpoint = async (jobData: TCreateFolderTreeCheckpointDTO, tx?: Knex) => {
|
||||
const { envId, folderCommitId, failedToAcquireLockCount = 0 } = jobData;
|
||||
|
||||
const latestTreeCheckpoint = await folderTreeCheckpointDAL.findLatestByEnvId(envId, tx);
|
||||
logger.info(`Folder tree checkpoint creation started [envId=${envId}] [attempt=${failedToAcquireLockCount + 1}]`);
|
||||
|
||||
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;
|
||||
// First, try to clear any stale locks before attempting to acquire
|
||||
if (failedToAcquireLockCount > 1) {
|
||||
try {
|
||||
await keyStore.deleteItem(KeyStorePrefixes.FolderTreeCheckpoint(envId));
|
||||
logger.info(`Cleared potential stale lock for envId ${envId} before attempt ${failedToAcquireLockCount + 1}`);
|
||||
} catch (error) {
|
||||
// This is fine if it fails, we'll still try to acquire the lock
|
||||
logger.info(`No stale lock found for envId ${envId}`);
|
||||
}
|
||||
}
|
||||
|
||||
const folders = await folderDAL.findByEnvId(envId, tx);
|
||||
const sortedFolders = sortFoldersByHierarchy(folders);
|
||||
const filteredFoldersIds = sortedFolders.filter((folder) => !folder.isReserved).map((folder) => folder.id);
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||
|
||||
const folderCommits = await folderCommitDAL.findMultipleLatestCommits(filteredFoldersIds, tx);
|
||||
const folderTreeCheckpoint = await folderTreeCheckpointDAL.create(
|
||||
{
|
||||
folderCommitId: latestCommitId
|
||||
},
|
||||
tx
|
||||
);
|
||||
try {
|
||||
// Attempt to acquire the lock with a shorter timeout for first attempts
|
||||
const timeout = failedToAcquireLockCount > 3 ? 60 * 1000 : 15 * 1000;
|
||||
|
||||
await folderTreeCheckpointResourcesDAL.insertMany(
|
||||
folderCommits.map((folderCommit) => ({
|
||||
folderTreeCheckpointId: folderTreeCheckpoint.id,
|
||||
folderId: folderCommit.folderId,
|
||||
folderCommitId: folderCommit.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
logger.info(`Attempting to acquire lock for envId=${envId} with timeout ${timeout}ms`);
|
||||
|
||||
logger.info("Folder tree checkpoint created successfully:", folderTreeCheckpoint.id);
|
||||
lock = await keyStore.acquireLock([KeyStorePrefixes.FolderTreeCheckpoint(envId)], timeout);
|
||||
|
||||
logger.info(`Successfully acquired lock for envId=${envId}`);
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`Failed to acquire lock for folder tree checkpoint [envId=${envId}] [attempt=${failedToAcquireLockCount + 1}]`
|
||||
);
|
||||
|
||||
// Requeue with incremented failure count if under max attempts
|
||||
if (failedToAcquireLockCount < 10) {
|
||||
// Force a delay between retries
|
||||
const nextRetryCount = failedToAcquireLockCount + 1;
|
||||
|
||||
logger.info(`Scheduling retry #${nextRetryCount} for folder tree checkpoint [envId=${envId}]`);
|
||||
|
||||
// Create a new job with incremented counter
|
||||
await scheduleTreeCheckpoint({
|
||||
envId,
|
||||
folderCommitId,
|
||||
failedToAcquireLockCount: nextRetryCount
|
||||
});
|
||||
} else {
|
||||
// Max retries reached
|
||||
logger.error(`Maximum lock acquisition attempts (10) reached for envId ${envId}. Giving up.`);
|
||||
// Try to force-clear the lock for next time
|
||||
try {
|
||||
await keyStore.deleteItem(KeyStorePrefixes.FolderTreeCheckpoint(envId));
|
||||
} catch (clearError) {
|
||||
logger.error(clearError, `Failed to clear lock after maximum retries for envId=${envId}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lock) {
|
||||
logger.error(`Lock is undefined after acquisition for envId=${envId}. This should never happen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Processing tree checkpoint data for envId=${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}`);
|
||||
} catch (error) {
|
||||
logger.error(error, `Error processing folder tree checkpoint [envId=${envId}]`);
|
||||
throw error;
|
||||
} finally {
|
||||
// Always release the lock
|
||||
try {
|
||||
if (lock) {
|
||||
await lock.release();
|
||||
logger.info(`Released lock for folder tree checkpoint [envId=${envId}]`);
|
||||
} else {
|
||||
logger.error(`No lock to release for envId=${envId}. This should never happen.`);
|
||||
}
|
||||
} catch (releaseError) {
|
||||
logger.error(releaseError, `Error releasing lock for folder tree checkpoint [envId=${envId}]`);
|
||||
// Try to force delete the lock if release fails
|
||||
try {
|
||||
await keyStore.deleteItem(KeyStorePrefixes.FolderTreeCheckpoint(envId));
|
||||
logger.info(`Force deleted lock after release failure for envId=${envId}`);
|
||||
} catch (deleteError) {
|
||||
logger.error(deleteError, `Failed to force delete lock after release failure for envId=${envId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
queueService.start(QueueName.FolderTreeCheckpoint, async (job) => {
|
||||
try {
|
||||
if (job.name === QueueJobs.CreateFolderTreeCheckpoint) {
|
||||
const { envId } = job.data as { envId: string };
|
||||
await createFolderTreeCheckpoint(envId);
|
||||
const jobData = job.data as TCreateFolderTreeCheckpointDTO;
|
||||
await createFolderTreeCheckpoint(jobData);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, "Error creating folder tree checkpoint:");
|
||||
@@ -189,9 +275,8 @@ export const folderCommitQueueServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
scheduleTreeCheckpoint,
|
||||
schedulePeriodicTreeCheckpoint,
|
||||
cancelScheduledTreeCheckpoint,
|
||||
createFolderTreeCheckpoint
|
||||
scheduleTreeCheckpoint: (envId: string) => scheduleTreeCheckpoint({ envId }),
|
||||
createFolderTreeCheckpoint: (envId: string, folderCommitId?: string, tx?: Knex) =>
|
||||
createFolderTreeCheckpoint({ envId, folderCommitId }, tx)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,12 @@ import { TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { ChangeType, folderCommitServiceFactory, TFolderCommitServiceFactory } from "./folder-commit-service";
|
||||
import {
|
||||
ChangeType,
|
||||
CommitType,
|
||||
folderCommitServiceFactory,
|
||||
TFolderCommitServiceFactory
|
||||
} from "./folder-commit-service";
|
||||
|
||||
// Mock config
|
||||
vi.mock("@app/lib/config/env", () => ({
|
||||
@@ -204,7 +209,7 @@ describe("folderCommitServiceFactory", () => {
|
||||
folderId: folderData.id,
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
type: CommitType.ADD,
|
||||
secretVersionId: "secret-version-1"
|
||||
}
|
||||
]
|
||||
@@ -264,7 +269,7 @@ describe("folderCommitServiceFactory", () => {
|
||||
folderId: folderData.id,
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
type: CommitType.ADD,
|
||||
folderVersionId: "folder-version-1"
|
||||
}
|
||||
]
|
||||
@@ -331,7 +336,7 @@ describe("folderCommitServiceFactory", () => {
|
||||
|
||||
const data = {
|
||||
folderCommitId: commitData.id,
|
||||
changeType: "add",
|
||||
changeType: CommitType.ADD,
|
||||
secretVersionId: "secret-version-1"
|
||||
};
|
||||
|
||||
@@ -348,7 +353,7 @@ describe("folderCommitServiceFactory", () => {
|
||||
// Arrange
|
||||
const data = {
|
||||
folderCommitId: "commit-id",
|
||||
changeType: "add"
|
||||
changeType: CommitType.ADD
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
@@ -361,7 +366,7 @@ describe("folderCommitServiceFactory", () => {
|
||||
|
||||
const data = {
|
||||
folderCommitId: "non-existent-commit",
|
||||
changeType: "add",
|
||||
changeType: CommitType.ADD,
|
||||
secretVersionId: "secret-version-1"
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ export enum ChangeType {
|
||||
CREATE = "create"
|
||||
}
|
||||
|
||||
export enum CommitType {
|
||||
ADD = "add",
|
||||
DELETE = "delete"
|
||||
}
|
||||
|
||||
enum ResourceType {
|
||||
SECRET = "secret",
|
||||
FOLDER = "folder"
|
||||
@@ -68,11 +73,12 @@ export type ResourceChange = {
|
||||
versionId: string;
|
||||
oldVersionId?: string;
|
||||
changeType: ChangeType;
|
||||
commitId: number;
|
||||
commitId: bigint;
|
||||
createdAt?: Date;
|
||||
parentId?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: string;
|
||||
@@ -209,7 +215,7 @@ export const folderCommitServiceFactory = ({
|
||||
const latestCheckpoint = await folderCheckpointDAL.findLatestByFolderId(folderId, tx);
|
||||
|
||||
if (!latestCommitId) {
|
||||
const latestCommit = await folderCommitDAL.findLatestCommit(folderId, tx);
|
||||
const latestCommit = await folderCommitDAL.findLatestCommit(folderId, undefined, tx);
|
||||
if (!latestCommit) {
|
||||
throw new BadRequestError({ message: "Latest commit ID not found" });
|
||||
}
|
||||
@@ -264,7 +270,11 @@ export const folderCommitServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Commit with ID ${folderCommitId} not found` });
|
||||
}
|
||||
|
||||
const nearestCheckpoint = await folderCheckpointDAL.findNearestCheckpoint(folderCommitId, tx);
|
||||
const nearestCheckpoint = await folderCheckpointDAL.findNearestCheckpoint(
|
||||
targetCommit.commitId,
|
||||
targetCommit.folderId,
|
||||
tx
|
||||
);
|
||||
if (!nearestCheckpoint) {
|
||||
throw new NotFoundError({ message: `Nearest checkpoint not found for commit ${folderCommitId}` });
|
||||
}
|
||||
@@ -288,7 +298,7 @@ export const folderCommitServiceFactory = ({
|
||||
checkpointResources.forEach((resource) => {
|
||||
if (resource.secretVersionId && resource.referencedSecretId) {
|
||||
folderState[`secret-${resource.referencedSecretId}`] = {
|
||||
type: "secret",
|
||||
type: ResourceType.SECRET,
|
||||
id: resource.referencedSecretId,
|
||||
versionId: resource.secretVersionId,
|
||||
secretKey: resource.secretKey,
|
||||
@@ -296,7 +306,7 @@ export const folderCommitServiceFactory = ({
|
||||
};
|
||||
} else if (resource.folderVersionId && resource.referencedFolderId) {
|
||||
folderState[`folder-${resource.referencedFolderId}`] = {
|
||||
type: "folder",
|
||||
type: ResourceType.FOLDER,
|
||||
id: resource.referencedFolderId,
|
||||
versionId: resource.folderVersionId,
|
||||
folderName: resource.folderName,
|
||||
@@ -323,7 +333,7 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
if (change.changeType.toLowerCase() === "add") {
|
||||
folderState[key] = {
|
||||
type: "secret",
|
||||
type: ResourceType.SECRET,
|
||||
id: change.referencedSecretId,
|
||||
versionId: change.secretVersionId,
|
||||
secretKey: change.secretKey,
|
||||
@@ -337,7 +347,7 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
if (change.changeType.toLowerCase() === "add") {
|
||||
folderState[key] = {
|
||||
type: "folder",
|
||||
type: ResourceType.FOLDER,
|
||||
id: change.referencedFolderId,
|
||||
versionId: change.folderVersionId,
|
||||
folderName: change.folderName,
|
||||
@@ -902,12 +912,14 @@ export const folderCommitServiceFactory = ({
|
||||
const secretVersions = await secretVersionV2BridgeDAL.findByIdsWithLatestVersion(
|
||||
folderId,
|
||||
secretChanges.map((diff) => diff.id),
|
||||
secretChanges.map((diff) => diff.versionId)
|
||||
secretChanges.map((diff) => diff.versionId),
|
||||
tx
|
||||
);
|
||||
|
||||
const folderVersions = await folderVersionDAL.findByIdsWithLatestVersion(
|
||||
folderChanges.map((diff) => diff.id),
|
||||
folderChanges.map((diff) => diff.versionId)
|
||||
folderChanges.map((diff) => diff.versionId),
|
||||
tx
|
||||
);
|
||||
|
||||
// Process changes in parallel
|
||||
@@ -1072,8 +1084,8 @@ export const folderCommitServiceFactory = ({
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
const changes = await folderCommitChangesDAL.findByCommitId(commitId);
|
||||
const commit = await folderCommitDAL.findById(commitId);
|
||||
const changes = await folderCommitChangesDAL.findByCommitId(commitId, projectId);
|
||||
const commit = await folderCommitDAL.findById(commitId, undefined, projectId);
|
||||
return { ...commit, changes };
|
||||
};
|
||||
|
||||
@@ -1244,8 +1256,6 @@ export const folderCommitServiceFactory = ({
|
||||
}: {
|
||||
targetCommitId: string;
|
||||
envId: string;
|
||||
actorId: string;
|
||||
actorType: ActorType;
|
||||
projectId: string;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
@@ -1254,7 +1264,7 @@ export const folderCommitServiceFactory = ({
|
||||
throw new NotFoundError({ message: `No commit found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
|
||||
const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommitId, envId, tx);
|
||||
const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommit.commitId, envId, tx);
|
||||
if (!checkpoint) {
|
||||
throw new NotFoundError({ message: `No checkpoint found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
@@ -1267,7 +1277,7 @@ export const folderCommitServiceFactory = ({
|
||||
});
|
||||
|
||||
// Group commits by folderId and keep only the latest
|
||||
const folderGroups = new Map<string, { commitId: number; id: string }>();
|
||||
const folderGroups = new Map<string, { commitId: bigint; id: string }>();
|
||||
|
||||
if (folderCheckpointCommits && folderCheckpointCommits.length > 0) {
|
||||
for (const commit of folderCheckpointCommits) {
|
||||
@@ -1358,7 +1368,7 @@ export const folderCommitServiceFactory = ({
|
||||
throw new NotFoundError({ message: `No commit found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
|
||||
const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommitId, envId, tx);
|
||||
const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommit.commitId, envId, tx);
|
||||
if (!checkpoint) {
|
||||
throw new NotFoundError({ message: `No checkpoint found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
@@ -1371,7 +1381,7 @@ export const folderCommitServiceFactory = ({
|
||||
});
|
||||
|
||||
// Group commits by folderId and keep only the latest
|
||||
const folderGroups = new Map<string, { commitId: number; id: string }>();
|
||||
const folderGroups = new Map<string, { commitId: bigint; id: string }>();
|
||||
|
||||
if (folderCheckpointCommits && folderCheckpointCommits.length > 0) {
|
||||
for (const commit of folderCheckpointCommits) {
|
||||
@@ -1421,9 +1431,9 @@ export const folderCommitServiceFactory = ({
|
||||
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 changes of folderDiffs.values()) {
|
||||
for (const change of changes) {
|
||||
if (change.changeType === ChangeType.DELETE && change.type === "folder") {
|
||||
if (change.changeType === ChangeType.DELETE && change.type === ResourceType.FOLDER) {
|
||||
foldersToDelete.add(change.id);
|
||||
}
|
||||
}
|
||||
@@ -1483,7 +1493,7 @@ export const folderCommitServiceFactory = ({
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
return folderCommitDAL.findLatestCommit(folderId);
|
||||
return folderCommitDAL.findLatestCommit(folderId, projectId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1506,6 +1516,19 @@ export const folderCommitServiceFactory = ({
|
||||
projectId: string;
|
||||
message?: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCommitsActions.PerformRollback,
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
// Check permissions first
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
@@ -1516,7 +1539,7 @@ export const folderCommitServiceFactory = ({
|
||||
});
|
||||
|
||||
// Get the commit to revert
|
||||
const commitToRevert = await folderCommitDAL.findById(commitId);
|
||||
const commitToRevert = await folderCommitDAL.findById(commitId, undefined, projectId);
|
||||
if (!commitToRevert) {
|
||||
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const secretVersionSchema = z.object({
|
||||
secretKey: z.string(),
|
||||
secretComment: z.string().optional().nullable(),
|
||||
skipMultilineEncoding: z.boolean().optional().nullable(),
|
||||
secretReminderRepeatDays: z.number().optional().nullable(),
|
||||
secretReminderNote: z.string().optional().nullable(),
|
||||
metadata: z.unknown().optional().nullable(),
|
||||
tags: z.array(z.string()).optional().nullable(),
|
||||
secretReminderRecipients: z.array(z.any()).optional().nullable(),
|
||||
secretValue: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
const folderVersionSchema = z.object({
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
const baseChangeSchema = z.object({
|
||||
id: z.string(),
|
||||
folderCommitId: z.string(),
|
||||
changeType: z.string(),
|
||||
isUpdate: z.boolean().optional(),
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
actorMetadata: z
|
||||
.union([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional()
|
||||
}),
|
||||
z.unknown()
|
||||
])
|
||||
.optional(),
|
||||
actorType: z.string().optional(),
|
||||
message: z.string().optional().nullable(),
|
||||
folderId: z.string().optional()
|
||||
});
|
||||
|
||||
const secretChangeSchema = baseChangeSchema.extend({
|
||||
objectType: z.literal("secret"),
|
||||
secretVersionId: z.string(),
|
||||
folderVersionId: z.null(),
|
||||
folderName: z.null(),
|
||||
folderChangeId: z.null(),
|
||||
secretKey: z.string(),
|
||||
secretVersion: z.union([z.string(), z.number()]),
|
||||
secretId: z.string(),
|
||||
versions: z.array(secretVersionSchema).optional()
|
||||
});
|
||||
|
||||
const folderChangeSchema = baseChangeSchema.extend({
|
||||
objectType: z.literal("folder"),
|
||||
secretVersionId: z.null(),
|
||||
folderVersionId: z.string(),
|
||||
folderName: z.string(),
|
||||
folderChangeId: z.string(),
|
||||
folderVersion: z.union([z.string(), z.number()]),
|
||||
secretKey: z.null(),
|
||||
secretId: z.null(),
|
||||
versions: z.array(folderVersionSchema).optional()
|
||||
});
|
||||
|
||||
const commitChangeSchema = z.discriminatedUnion("objectType", [secretChangeSchema, folderChangeSchema]);
|
||||
|
||||
const commitSchema = z.object({
|
||||
id: z.string(),
|
||||
commitId: z.string(),
|
||||
actorMetadata: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
actorType: z.string(),
|
||||
message: z.string().nullable(),
|
||||
folderId: z.string(),
|
||||
envId: z.string(),
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
changes: z.array(commitChangeSchema)
|
||||
});
|
||||
|
||||
export const commitChangesResponseSchema = z.object({
|
||||
changes: commitSchema
|
||||
});
|
||||
Reference in New Issue
Block a user