mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Type fixes and PIT history pagination
This commit is contained in:
@@ -84,9 +84,10 @@ const getZodDefaultValue = (type: unknown, value: string | number | boolean | Ob
|
||||
}
|
||||
};
|
||||
|
||||
const bigIntegerColumns = {
|
||||
const bigIntegerColumns: Record<string, string[]> = {
|
||||
"folder_commits": ["commitId"]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const main = async () => {
|
||||
const tables = (
|
||||
|
||||
@@ -10,8 +10,8 @@ import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ChangeType, ResourceChange } from "@app/services/folder-commit/folder-commit-service";
|
||||
import { ActorAuthMethod, ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
|
||||
import { commitChangesResponseSchema } from "@app/services/folder-commit/folder-commit-types";
|
||||
|
||||
const commitHistoryItemSchema = z.object({
|
||||
@@ -98,22 +98,34 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
workspaceId: z.string().trim()
|
||||
workspaceId: z.string().trim(),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
search: z.string().trim().optional(),
|
||||
sort: z.enum(["asc", "desc"]).default("desc")
|
||||
}),
|
||||
response: {
|
||||
200: commitHistoryItemSchema.array()
|
||||
200: z.object({
|
||||
commits: commitHistoryItemSchema.array(),
|
||||
total: z.number(),
|
||||
hasMore: z.boolean()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const commits = await server.services.folderCommit.getCommitsForFolder({
|
||||
const result = await server.services.folderCommit.getCommitsForFolder({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.query.workspaceId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
path: req.query.path,
|
||||
offset: req.query.offset,
|
||||
limit: req.query.limit,
|
||||
search: req.query.search,
|
||||
sort: req.query.sort
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@@ -124,30 +136,47 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
path: req.query.path,
|
||||
commitCount: commits.length.toString()
|
||||
commitCount: result.commits.length.toString(),
|
||||
offset: req.query.offset.toString(),
|
||||
limit: req.query.limit.toString(),
|
||||
search: req.query.search,
|
||||
sort: req.query.sort
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return commits.map((commit) => ({
|
||||
...commit,
|
||||
commitId: commit.commitId.toString()
|
||||
}));
|
||||
return {
|
||||
commits: result.commits.map((commit) => ({
|
||||
...commit,
|
||||
commitId: commit.commitId.toString()
|
||||
})),
|
||||
total: result.total,
|
||||
hasMore: result.hasMore
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const getChangeVersions = async (
|
||||
change: ResourceChange,
|
||||
change: {
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
id?: string;
|
||||
isUpdate?: boolean;
|
||||
changeType?: string;
|
||||
},
|
||||
previousVersion: string,
|
||||
actorId: string,
|
||||
actor: string,
|
||||
actor: ActorType,
|
||||
actorOrgId: string,
|
||||
actorAuthMethod: string,
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
folderId: string
|
||||
) => {
|
||||
if (change.secretVersion) {
|
||||
const currentVersion = change.secretVersion || "1";
|
||||
const secretId = change.secretId ? change.secretId : change.id;
|
||||
if (!secretId) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
||||
actorId,
|
||||
@@ -176,7 +205,15 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getFolderVersions = async (change: ResourceChange, fromVersion: string, folderId: string) => {
|
||||
const getFolderVersions = async (
|
||||
change: {
|
||||
folderVersion?: string;
|
||||
isUpdate?: boolean;
|
||||
changeType?: string;
|
||||
},
|
||||
fromVersion: string,
|
||||
folderId: string
|
||||
) => {
|
||||
const currentVersion = change.folderVersion || "1";
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.folder.getFolderVersionsByIds({
|
||||
@@ -218,20 +255,18 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||
commitId: req.params.commitId
|
||||
});
|
||||
for (const change of changes.changes) {
|
||||
if (change.secretVersionId) {
|
||||
change.objectType = "secret";
|
||||
if (change.secretVersionId && change.secretVersion) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
change.versions = await getChangeVersions(
|
||||
change,
|
||||
(Number.parseInt(change.secretVersion, 10) - 1).toString(),
|
||||
req.permission?.id,
|
||||
req.permission?.type,
|
||||
req.permission?.orgId,
|
||||
req.permission?.authMethod,
|
||||
req.permission.id,
|
||||
req.permission.type,
|
||||
req.permission.orgId,
|
||||
req.permission.authMethod,
|
||||
change.folderId
|
||||
);
|
||||
} else if (change.folderVersionId && change.folderChangeId) {
|
||||
change.objectType = "folder";
|
||||
} else if (change.folderVersionId && change.folderChangeId && change.folderVersion) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
change.versions = await getFolderVersions(
|
||||
change,
|
||||
@@ -340,10 +375,10 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||
change.versions = await getChangeVersions(
|
||||
change,
|
||||
change.fromVersion || "1",
|
||||
req.permission?.id,
|
||||
req.permission?.type,
|
||||
req.permission?.orgId,
|
||||
req.permission?.authMethod,
|
||||
req.permission.id,
|
||||
req.permission.type,
|
||||
req.permission.orgId,
|
||||
req.permission.authMethod,
|
||||
diff.folderId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2927,6 +2927,10 @@ interface GetProjectPitCommitsEvent {
|
||||
commitCount: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
offset: string;
|
||||
limit: string;
|
||||
search?: string;
|
||||
sort: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export const folderCheckpointDALFactory = (db: TDbClient) => {
|
||||
`${TableName.FolderCommit}.id`
|
||||
)
|
||||
.where(`${TableName.FolderCommit}.folderId`, "=", folderId)
|
||||
.where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId)
|
||||
.where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString())
|
||||
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||
.select(
|
||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||
|
||||
@@ -14,7 +14,7 @@ import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
||||
|
||||
type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||
export type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||
actorMetadata: unknown;
|
||||
actorType: string;
|
||||
message?: string | null;
|
||||
@@ -25,6 +25,7 @@ type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
folderChangeId?: string;
|
||||
objectType?: string;
|
||||
versions?: {
|
||||
secretKey?: string;
|
||||
secretComment?: string;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TSecretFolderVersions,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderCommitDALFactory = ReturnType<typeof folderCommitDALFactory>;
|
||||
@@ -209,8 +209,8 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
const commits = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||
.andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber)
|
||||
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber)
|
||||
.andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber.toString())
|
||||
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber.toString())
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy(`${TableName.FolderCommit}.commitId`, "asc");
|
||||
|
||||
@@ -391,12 +391,89 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy("commitId", "desc")
|
||||
.first();
|
||||
if (!doc) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder commit not found for ID ${id}`
|
||||
});
|
||||
}
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindById" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderIdPaginated = async (
|
||||
folderId: string,
|
||||
options: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
sort?: "asc" | "desc";
|
||||
} = {},
|
||||
tx?: Knex
|
||||
): Promise<{
|
||||
commits: TFolderCommits[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
}> => {
|
||||
try {
|
||||
const { offset = 0, limit = 20, search, sort = "desc" } = options;
|
||||
const trx = tx || db.replicaNode();
|
||||
|
||||
// Build base query
|
||||
let baseQuery = trx(TableName.FolderCommit).where({ folderId });
|
||||
|
||||
// Add search functionality
|
||||
if (search) {
|
||||
baseQuery = baseQuery.where((qb) => {
|
||||
void qb.whereILike("message", `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
// Get total count
|
||||
const totalResult = await baseQuery.clone().count("*", { as: "count" }).first();
|
||||
const total = Number(totalResult?.count || 0);
|
||||
|
||||
// Get paginated commits
|
||||
const folderCommits = await baseQuery.select("*").orderBy("createdAt", sort).limit(limit).offset(offset);
|
||||
|
||||
if (folderCommits.length === 0) {
|
||||
return { commits: [], total, hasMore: false };
|
||||
}
|
||||
|
||||
// Get all commit IDs for changes
|
||||
const commitIds = folderCommits.map((commit) => commit.id);
|
||||
|
||||
// Get all related changes
|
||||
const changes = await trx(TableName.FolderCommitChanges).whereIn("folderCommitId", commitIds).select("*");
|
||||
|
||||
const changesMap = changes.reduce(
|
||||
(acc, change) => {
|
||||
const { folderCommitId } = change;
|
||||
if (!acc[folderCommitId]) acc[folderCommitId] = [];
|
||||
acc[folderCommitId].push(change);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, TFolderCommitChanges[]>
|
||||
);
|
||||
|
||||
const commitsWithChanges = folderCommits.map((commit) => ({
|
||||
...commit,
|
||||
changes: changesMap[commit.id] || []
|
||||
}));
|
||||
|
||||
const hasMore = offset + limit < total;
|
||||
|
||||
return {
|
||||
commits: commitsWithChanges,
|
||||
total,
|
||||
hasMore
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByFolderIdPaginated" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...restOfOrm,
|
||||
findByFolderId,
|
||||
@@ -411,6 +488,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
findLatestCommitByFolderIds,
|
||||
findAllFolderCommitsAfter,
|
||||
findPreviousCommitTo,
|
||||
findById
|
||||
findById,
|
||||
findByFolderIdPaginated
|
||||
};
|
||||
};
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("folderCommitServiceFactory", () => {
|
||||
deleteById: vi.fn().mockResolvedValue({}),
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
updateById: vi.fn().mockResolvedValue({}),
|
||||
find: vi.fn().mockResolvedValue([]),
|
||||
find: vi.fn().mockResolvedValue({}), // Changed from [] to {} to match Object.values() expectation
|
||||
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
|
||||
};
|
||||
|
||||
@@ -260,6 +260,17 @@ describe("folderCommitServiceFactory", () => {
|
||||
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||
|
||||
// Mock folderVersionDAL.find to return an object with folder version data
|
||||
mockFolderVersionDAL.find.mockResolvedValue({
|
||||
"folder-version-1": {
|
||||
id: "folder-version-1",
|
||||
folderId: "sub-folder-id",
|
||||
envId: "env-id",
|
||||
name: "Test Folder",
|
||||
version: 1
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
actor: {
|
||||
type: ActorType.IDENTITY,
|
||||
@@ -449,6 +460,12 @@ describe("folderCommitServiceFactory", () => {
|
||||
const actorType = ActorType.USER;
|
||||
const projectId = "project-id";
|
||||
|
||||
// Mock the transaction to properly handle the error
|
||||
mockFolderCommitDAL.transaction.mockImplementation(async (callback) => {
|
||||
return await callback({} as Knex);
|
||||
});
|
||||
|
||||
// Mock findById to return null inside the transaction
|
||||
mockFolderCommitDAL.findById.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
@@ -501,8 +518,8 @@ describe("folderCommitServiceFactory", () => {
|
||||
const actorType = ActorType.USER;
|
||||
|
||||
const differences = [
|
||||
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: 1 },
|
||||
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: 1 }
|
||||
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: BigInt(1) },
|
||||
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: BigInt(1) }
|
||||
];
|
||||
|
||||
const secretVersions = {
|
||||
|
||||
@@ -76,6 +76,7 @@ export type ResourceChange = {
|
||||
commitId: bigint;
|
||||
createdAt?: Date;
|
||||
parentId?: string;
|
||||
isUpdate?: boolean;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
@@ -997,7 +998,11 @@ export const folderCommitServiceFactory = ({
|
||||
actorOrgId,
|
||||
projectId,
|
||||
environment,
|
||||
path
|
||||
path,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
search,
|
||||
sort = "desc"
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
@@ -1006,6 +1011,10 @@ export const folderCommitServiceFactory = ({
|
||||
projectId: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
search?: string;
|
||||
sort: "asc" | "desc";
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
@@ -1020,7 +1029,12 @@ export const folderCommitServiceFactory = ({
|
||||
message: `Folder not found for project ID ${projectId}, environment ${environment}, path ${path}`
|
||||
});
|
||||
}
|
||||
const folderCommits = await folderCommitDAL.findByFolderId(folder.id);
|
||||
const folderCommits = await folderCommitDAL.findByFolderIdPaginated(folder.id, {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
sort
|
||||
});
|
||||
return folderCommits;
|
||||
};
|
||||
|
||||
@@ -1516,6 +1530,9 @@ export const folderCommitServiceFactory = ({
|
||||
projectId: string;
|
||||
message?: string;
|
||||
}) => {
|
||||
if (!permissionService) {
|
||||
throw new Error("Permission service not initialized");
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -1551,8 +1568,11 @@ export const folderCommitServiceFactory = ({
|
||||
}
|
||||
|
||||
// Sort commits by commitId (which appears to be numeric)
|
||||
const sortedCommits = allCommits.sort((a, b) => a.commitId - b.commitId);
|
||||
|
||||
const sortedCommits = allCommits.sort((a, b) => {
|
||||
if (a.commitId < b.commitId) return -1;
|
||||
if (a.commitId > b.commitId) return 1;
|
||||
return 0;
|
||||
});
|
||||
// Find the index of the commit to revert
|
||||
const commitIndex = sortedCommits.findIndex((c) => c.id === commitId);
|
||||
if (commitIndex === -1) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const secretVersionSchema = z.object({
|
||||
secretKey: z.string(),
|
||||
secretKey: z.string().optional().nullable(),
|
||||
secretComment: z.string().optional().nullable(),
|
||||
skipMultilineEncoding: z.boolean().optional().nullable(),
|
||||
secretReminderRepeatDays: z.number().optional().nullable(),
|
||||
@@ -13,7 +13,7 @@ const secretVersionSchema = z.object({
|
||||
});
|
||||
|
||||
const folderVersionSchema = z.object({
|
||||
name: z.string()
|
||||
name: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
const baseChangeSchema = z.object({
|
||||
@@ -37,46 +37,37 @@ const baseChangeSchema = z.object({
|
||||
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 commitChangeSchema = baseChangeSchema.extend({
|
||||
secretVersionId: z.string().optional().nullable(),
|
||||
folderVersionId: z.string().optional().nullable(),
|
||||
folderName: z.string().optional().nullable(),
|
||||
folderChangeId: z.string().optional().nullable(),
|
||||
secretKey: z.string().optional().nullable(),
|
||||
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
secretId: z.string().optional().nullable(),
|
||||
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
versions: z.array(z.union([secretVersionSchema, folderVersionSchema])).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()
|
||||
}),
|
||||
actorMetadata: z
|
||||
.union([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional()
|
||||
}),
|
||||
z.unknown()
|
||||
])
|
||||
.optional(),
|
||||
actorType: z.string(),
|
||||
message: z.string().nullable(),
|
||||
message: z.string().nullable().optional(),
|
||||
folderId: z.string(),
|
||||
envId: z.string(),
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
changes: z.array(commitChangeSchema)
|
||||
changes: z.array(commitChangeSchema).optional()
|
||||
});
|
||||
|
||||
export const commitChangesResponseSchema = z.object({
|
||||
|
||||
@@ -41,7 +41,7 @@ export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||
.where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId)
|
||||
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString())
|
||||
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
||||
.select(db.ref("commitId").withSchema(TableName.FolderCommit))
|
||||
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
||||
|
||||
@@ -145,7 +145,9 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
||||
|
||||
// Create lookup map for max versions
|
||||
const maxVersionMap = maxVersionsQuery.reduce<Record<string, number>>((acc, item) => {
|
||||
acc[item.folderId] = item.maxVersion;
|
||||
if (item.maxVersion) {
|
||||
acc[item.folderId] = item.maxVersion;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@@ -160,6 +162,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
if (!folderIds.length && (!versionIds || !versionIds.length)) return {};
|
||||
|
||||
// Run both queries in parallel
|
||||
const [latestVersions, specificVersionsWithLatest] = await Promise.all([
|
||||
folderIds.length ? getLatestFolderVersions(folderIds, tx) : [],
|
||||
versionIds?.length ? getSpecificFolderVersionsWithLatest(versionIds, tx) : []
|
||||
|
||||
@@ -324,7 +324,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.whereIn("secretId", specificSecretIds)
|
||||
.groupBy("secretId")
|
||||
.select("secretId")
|
||||
.max("version as maxVersion");
|
||||
.max("version", { as: "maxVersion" });
|
||||
|
||||
// Create a lookup map for max versions
|
||||
const maxVersionMap = maxVersionsQuery.reduce(
|
||||
|
||||
@@ -68,13 +68,29 @@ const fetchFolderCommitsCount = async ({
|
||||
const fetchFolderCommitHistory = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
directory: string
|
||||
): Promise<CommitHistoryItem[]> => {
|
||||
const res = await apiRequest.get<CommitHistoryItem[]>("/api/v1/pit/commits", {
|
||||
directory: string,
|
||||
offset: number = 0,
|
||||
limit: number = 20,
|
||||
search?: string,
|
||||
sort: "asc" | "desc" = "desc"
|
||||
): Promise<{
|
||||
commits: CommitHistoryItem[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
}> => {
|
||||
const res = await apiRequest.get<{
|
||||
commits: CommitHistoryItem[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
}>("/api/v1/pit/commits", {
|
||||
params: {
|
||||
environment,
|
||||
path: directory,
|
||||
workspaceId
|
||||
workspaceId,
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
sort
|
||||
}
|
||||
});
|
||||
return res.data;
|
||||
@@ -225,15 +241,30 @@ export const useGetFolderCommitsCount = ({
|
||||
export const useGetFolderCommitHistory = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
directory
|
||||
directory,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
search,
|
||||
sort = "desc"
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
directory: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
sort?: "asc" | "desc";
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: commitKeys.history({ workspaceId, environment, directory }),
|
||||
queryFn: () => fetchFolderCommitHistory(workspaceId, environment, directory),
|
||||
queryKey: [
|
||||
commitKeys.history({ workspaceId, environment, directory }),
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
sort
|
||||
],
|
||||
queryFn: () =>
|
||||
fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort),
|
||||
enabled: Boolean(workspaceId && environment)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,7 +11,8 @@ export enum ApprovalStatus {
|
||||
export enum CommitType {
|
||||
DELETE = "delete",
|
||||
UPDATE = "update",
|
||||
CREATE = "create"
|
||||
CREATE = "create",
|
||||
ADD = "add"
|
||||
}
|
||||
|
||||
export type TSecretApprovalSecChangeData = {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { CommitWithChanges } from "@app/hooks/api/folderCommits";
|
||||
import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries";
|
||||
import { CommitType } from "@app/hooks/api/types";
|
||||
|
||||
import { SecretVersionDiffView } from "../SecretVersionDiffView";
|
||||
import { MergedItem } from "./types";
|
||||
@@ -147,9 +148,9 @@ export const CommitDetailsTab = ({
|
||||
const commitChanges = parsedCommitDetails.changes?.changes || [];
|
||||
|
||||
// Separate changes by type
|
||||
const addedChanges = commitChanges.filter((c) => c.changeType === "add" && !c.isUpdate);
|
||||
const updatedChanges = commitChanges.filter((c) => c.changeType === "add" && c.isUpdate);
|
||||
const deletedChanges = commitChanges.filter((c) => c.changeType === "delete");
|
||||
const addedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && !c.isUpdate);
|
||||
const updatedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && c.isUpdate);
|
||||
const deletedChanges = commitChanges.filter((c) => c.changeType === CommitType.DELETE);
|
||||
|
||||
// Create merged item list from changes only
|
||||
const changedItems: MergedItem[] = [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
faArrowDownWideShort,
|
||||
faArrowUpWideShort,
|
||||
@@ -144,41 +144,68 @@ export const CommitHistoryTab = ({
|
||||
secretPath: string;
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
||||
const [visibleCommits, setVisibleCommits] = useState(10);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [allCommits, setAllCommits] = useState<Commit[]>([]);
|
||||
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const limit = 5;
|
||||
|
||||
const { data: commits, isLoading } = useGetFolderCommitHistory({
|
||||
// Debounce search term
|
||||
useEffect(() => {
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current);
|
||||
}
|
||||
|
||||
debounceTimeoutRef.current = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [searchTerm]);
|
||||
|
||||
const {
|
||||
data: response,
|
||||
isLoading,
|
||||
isFetching
|
||||
} = useGetFolderCommitHistory({
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
directory: secretPath
|
||||
directory: secretPath,
|
||||
offset,
|
||||
limit,
|
||||
search: debouncedSearchTerm,
|
||||
sort: sortDirection
|
||||
});
|
||||
|
||||
const filteredCommits = useMemo(() => {
|
||||
if (!commits?.length) return [];
|
||||
const commits = response?.commits || [];
|
||||
const hasMore = response?.hasMore || false;
|
||||
|
||||
return commits.filter(
|
||||
(commit) =>
|
||||
commit.id?.includes(searchTerm) ||
|
||||
commit.message?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
commit.actorMetadata?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [commits, searchTerm]);
|
||||
// Reset accumulated commits when search or sort changes
|
||||
useEffect(() => {
|
||||
setAllCommits([]);
|
||||
setOffset(0);
|
||||
}, [debouncedSearchTerm, sortDirection]);
|
||||
|
||||
const sortedCommits = useMemo(() => {
|
||||
if (!filteredCommits?.length) return [];
|
||||
// Accumulate commits instead of replacing them
|
||||
useEffect(() => {
|
||||
if (commits.length > 0) {
|
||||
if (offset === 0) {
|
||||
// First load or after search/sort change - replace all commits
|
||||
setAllCommits(commits);
|
||||
} else {
|
||||
// Subsequent loads - append new commits
|
||||
setAllCommits((prev) => [...prev, ...commits]);
|
||||
}
|
||||
}
|
||||
}, [commits, offset]);
|
||||
|
||||
return [...filteredCommits].sort((a, b) => {
|
||||
const dateA = new Date(a.createdAt).getTime();
|
||||
const dateB = new Date(b.createdAt).getTime();
|
||||
return sortDirection === "desc" ? dateB - dateA : dateA - dateB;
|
||||
});
|
||||
}, [filteredCommits, sortDirection]);
|
||||
|
||||
const displayedCommits = useMemo(() => {
|
||||
return sortedCommits.slice(0, visibleCommits);
|
||||
}, [sortedCommits, visibleCommits]);
|
||||
const groupedCommits = useMemo(() => {
|
||||
return displayedCommits.reduce(
|
||||
return allCommits.reduce(
|
||||
(acc, commit) => {
|
||||
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
|
||||
if (!acc[date]) {
|
||||
@@ -189,25 +216,21 @@ export const CommitHistoryTab = ({
|
||||
},
|
||||
{} as Record<string, Commit[]>
|
||||
);
|
||||
}, [displayedCommits]);
|
||||
}, [allCommits]);
|
||||
|
||||
const handleSort = useCallback(() => {
|
||||
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
|
||||
}, []);
|
||||
|
||||
const loadMoreCommits = useCallback(() => {
|
||||
setVisibleCommits((prev) => prev + 10);
|
||||
const handleSearch = useCallback((value: string) => {
|
||||
setSearchTerm(value);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Spinner size="lg" aria-label="Loading commits" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasMoreCommits = sortedCommits.length > visibleCommits;
|
||||
const loadMoreCommits = useCallback(() => {
|
||||
if (hasMore && !isFetching) {
|
||||
setOffset((prev) => prev + limit);
|
||||
}
|
||||
}, [hasMore, isFetching, limit]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -217,7 +240,8 @@ export const CommitHistoryTab = ({
|
||||
<Input
|
||||
placeholder="Search commits..."
|
||||
className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20"
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
value={searchTerm}
|
||||
aria-label="Search commits"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400">
|
||||
@@ -239,43 +263,57 @@ export const CommitHistoryTab = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{Object.keys(groupedCommits).length > 0 ? (
|
||||
<>
|
||||
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
||||
<DateGroup
|
||||
key={date}
|
||||
date={date}
|
||||
commits={dateCommits}
|
||||
onSelectCommit={onSelectCommit}
|
||||
{isLoading && offset === 0 ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Spinner size="lg" aria-label="Loading commits" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{Object.keys(groupedCommits).length > 0 ? (
|
||||
<>
|
||||
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
||||
<DateGroup
|
||||
key={date}
|
||||
date={date}
|
||||
commits={dateCommits}
|
||||
onSelectCommit={onSelectCommit}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faSearch}
|
||||
className="text-white-500 mb-3 text-3xl"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faSearch}
|
||||
className="text-white-500 mb-3 text-3xl"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p>No matching commits found. Try a different search term.</p>
|
||||
</div>
|
||||
)}
|
||||
<p>No matching commits found. Try a different search term.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasMoreCommits && (
|
||||
<div className="flex justify-center pb-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="md"
|
||||
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
onClick={loadMoreCommits}
|
||||
aria-label="Load more commits"
|
||||
>
|
||||
Load more commits
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{hasMore && (
|
||||
<div className="flex justify-center pb-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="md"
|
||||
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
onClick={loadMoreCommits}
|
||||
disabled={isFetching}
|
||||
aria-label="Load more commits"
|
||||
>
|
||||
{isFetching ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
"Load more commits"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user